diff --git a/build.go b/build.go index d259986..dc09827 100644 --- a/build.go +++ b/build.go @@ -23,14 +23,17 @@ import ( "log/slog" "os" "path/filepath" + "strings" "github.com/leonelquinteros/gotext" "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" ) @@ -46,6 +49,11 @@ func BuildCmd() *cli.Command { Value: "alr.sh", Usage: gotext.Get("Path to the build script"), }, + &cli.StringFlag{ + Name: "script-package", + Aliases: []string{"sp"}, + Usage: gotext.Get("Specify package in script (for multi package script only)"), + }, &cli.StringFlag{ Name: "package", Aliases: []string{"p"}, @@ -59,30 +67,58 @@ 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 packages []string // Проверяем, установлен ли флаг script (-s) + repoDir := cfg.GetPaths(ctx).RepoDir + switch { case c.IsSet("script"): script = c.String("script") + packages = append(packages, c.String("script-package")) case c.IsSet("package"): + // TODO: handle multiple packages packageInput := c.String("package") - if filepath.Dir(packageInput) == "." { - // Не указана директория репозитория, используем 'default' как префикс - script = filepath.Join(config.GetPaths(ctx).RepoDir, "default", packageInput, "alr.sh") + + arr := strings.Split(packageInput, "/") + var packageSearch string + if len(arr) == 2 { + packageSearch = arr[1] } else { - // Используем путь с указанным репозиторием - script = filepath.Join(config.GetPaths(ctx).RepoDir, packageInput, "alr.sh") + packageSearch = arr[0] + } + + pkgs, _, _ := rs.FindPkgs(ctx, []string{packageSearch}) + pkg, ok := pkgs[packageSearch] + if len(pkg) < 1 || !ok { + slog.Error(gotext.Get("Package not found")) + os.Exit(1) + } + + if pkg[0].BasePkgName != "" { + script = filepath.Join(repoDir, pkg[0].Repository, pkg[0].BasePkgName, "alr.sh") + packages = append(packages, pkg[0].Name) + } else { + 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 +132,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.NewBuilder( + ctx, + types.BuildOpts{ + Packages: packages, + 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..b686468 100644 --- a/coverage-badge.svg +++ b/coverage-badge.svg @@ -11,7 +11,7 @@ coverage coverage - 19.2% - 19.2% + 20.8% + 20.8% diff --git a/install.go b/install.go index 54982b4..2706ada 100644 --- a/install.go +++ b/install.go @@ -29,9 +29,10 @@ import ( "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/db" + database "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/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" ) @@ -63,22 +64,52 @@ func InstallCmd() *cli.Command { os.Exit(1) } - if config.GetInstance(ctx).AutoPull(ctx) { - err := repos.Pull(ctx, config.Config(ctx).Repos) + 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) + } + + 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) } } - found, notFound, err := repos.FindPkgs(ctx, args.Slice()) + found, notFound, err := rs.FindPkgs(ctx, args.Slice()) if err != nil { slog.Error(gotext.Get("Error finding packages"), "err", err) os.Exit(1) } pkgs := cliutils.FlattenPkgs(ctx, found, "install", c.Bool("interactive")) - build.InstallPkgs(ctx, pkgs, notFound, types.BuildOpts{ + + opts := types.BuildOpts{ + Manager: mgr, + Clean: c.Bool("clean"), + Interactive: c.Bool("interactive"), + } + + info, err := distro.ParseOSRelease(ctx) + if err != nil { + slog.Error(gotext.Get("Error parsing os release"), "err", err) + os.Exit(1) + } + + builder := build.NewBuilder( + ctx, + opts, + rs, + info, + cfg, + ) + + builder.InstallPkgs(ctx, pkgs, notFound, types.BuildOpts{ Manager: mgr, Clean: c.Bool("clean"), Interactive: c.Bool("interactive"), @@ -86,6 +117,8 @@ func InstallCmd() *cli.Command { return nil }, BashComplete: func(c *cli.Context) { + cfg := config.New() + db := database.New(cfg) result, err := db.GetPkgs(c.Context, "true") if err != nil { slog.Error(gotext.Get("Error getting packages"), "err", err) @@ -94,7 +127,7 @@ func InstallCmd() *cli.Command { defer result.Close() for result.Next() { - var pkg db.Package + var pkg database.Package err = result.StructScan(&pkg) if err != nil { slog.Error(gotext.Get("Error iterating over packages"), "err", err) 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..c89bfb4 100644 --- a/internal/shutils/decoder/decoder.go +++ b/internal/shutils/decoder/decoder.go @@ -164,7 +164,10 @@ func (d *Decoder) DecodeVars(val any) error { return nil } -type ScriptFunc func(ctx context.Context, opts ...interp.RunnerOption) error +type ( + ScriptFunc func(ctx context.Context, opts ...interp.RunnerOption) error + ScriptFuncWithSubshell func(ctx context.Context, opts ...interp.RunnerOption) (*interp.Runner, error) +) // GetFunc returns a function corresponding to a bash function // with the given name @@ -197,6 +200,24 @@ func (d *Decoder) GetFuncP(name string, prepare PrepareFunc) (ScriptFunc, bool) }, true } +func (d *Decoder) GetFuncWithSubshell(name string) (ScriptFuncWithSubshell, 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/shutils/handlers/fakeroot.go b/internal/shutils/handlers/fakeroot.go index b8d02c5..bc0090b 100644 --- a/internal/shutils/handlers/fakeroot.go +++ b/internal/shutils/handlers/fakeroot.go @@ -29,9 +29,9 @@ import ( "syscall" "time" + "gitea.plemya-x.ru/Plemya-x/fakeroot" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/interp" - "gitea.plemya-x.ru/Plemya-x/fakeroot" ) // FakerootExecHandler was extracted from github.com/mvdan/sh/interp/handler.go diff --git a/internal/translations/default.pot b/internal/translations/default.pot index b0cc59f..5c6c28b 100644 --- a/internal/translations/default.pot +++ b/internal/translations/default.pot @@ -9,40 +9,56 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: build.go:41 +#: build.go:44 msgid "Build a local package" msgstr "" -#: build.go:47 +#: build.go:50 msgid "Path to the build script" msgstr "" -#: build.go:52 +#: build.go:55 +msgid "Specify package in script (for multi package script only)" +msgstr "" + +#: build.go:60 msgid "Name of the package to build and its repo (example: default/go-bin)" msgstr "" -#: build.go:57 +#: build.go:65 msgid "" "Build package from scratch even if there's an already built package available" msgstr "" -#: build.go:87 -msgid "Error pulling repositories" +#: build.go:75 +msgid "Error db init" msgstr "" -#: build.go:95 -msgid "Unable to detect a supported package manager on the system" -msgstr "" - -#: build.go:107 -msgid "Error building package" -msgstr "" - -#: build.go:114 -msgid "Error getting working directory" +#: build.go:105 +msgid "Package not found" msgstr "" #: build.go:123 +msgid "Error pulling repositories" +msgstr "" + +#: build.go:131 +msgid "Unable to detect a supported package manager on the system" +msgstr "" + +#: build.go:137 +msgid "Error parsing os release" +msgstr "" + +#: build.go:158 +msgid "Error building package" +msgstr "" + +#: build.go:165 +msgid "Error getting working directory" +msgstr "" + +#: build.go:174 msgid "Error moving the package" msgstr "" @@ -130,31 +146,31 @@ msgstr "" msgid "Error encoding script variables" msgstr "" -#: install.go:42 +#: install.go:43 msgid "Install a new package" msgstr "" -#: install.go:56 +#: install.go:57 msgid "Command install expected at least 1 argument, got %d" msgstr "" -#: install.go:91 +#: install.go:124 msgid "Error getting packages" msgstr "" -#: install.go:100 +#: install.go:133 msgid "Error iterating over packages" msgstr "" -#: install.go:113 +#: install.go:146 msgid "Remove an installed package" msgstr "" -#: install.go:118 +#: install.go:151 msgid "Command remove expected at least 1 argument, got %d" msgstr "" -#: install.go:130 +#: install.go:163 msgid "Error removing packages" msgstr "" @@ -222,11 +238,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 +309,70 @@ msgstr "" msgid "Error while running app" msgstr "" -#: pkg/build/build.go:108 +#: pkg/build/build.go:153 msgid "Failed to prompt user to view build script" msgstr "" -#: pkg/build/build.go:112 +#: pkg/build/build.go:157 msgid "Building package" msgstr "" -#: pkg/build/build.go:156 +#: pkg/build/build.go:228 msgid "Downloading sources" msgstr "" -#: pkg/build/build.go:168 +#: pkg/build/build.go:246 msgid "Building package metadata" msgstr "" -#: pkg/build/build.go:190 +#: pkg/build/build.go:268 msgid "Compressing package" msgstr "" -#: pkg/build/build.go:316 +#: pkg/build/build.go:419 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:433 msgid "This package is already installed" msgstr "" -#: pkg/build/build.go:355 +#: pkg/build/build.go:457 msgid "Installing build dependencies" msgstr "" -#: pkg/build/build.go:397 +#: pkg/build/build.go:498 msgid "Installing dependencies" msgstr "" -#: pkg/build/build.go:443 -msgid "Executing version()" +#: pkg/build/build.go:533 +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:584 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:647 +msgid "Executing prepare()" +msgstr "" + +#: pkg/build/build.go:657 +msgid "Executing build()" +msgstr "" + +#: pkg/build/build.go:687 pkg/build/build.go:707 +msgid "Executing %s()" +msgstr "" + +#: pkg/build/build.go:766 +msgid "Error installing native packages" +msgstr "" + +#: pkg/build/build.go:790 +msgid "Error installing package" msgstr "" #: pkg/build/findDeps.go:35 @@ -383,27 +387,27 @@ msgstr "" msgid "Required dependency found" msgstr "" -#: pkg/build/install.go:42 -msgid "Error installing native packages" +#: pkg/build/utils.go:133 +msgid "AutoProv is not implemented for this package format, so it's skipped" msgstr "" -#: pkg/build/install.go:79 -msgid "Error installing package" +#: pkg/build/utils.go:144 +msgid "AutoReq is not implemented for this package format, so it's skipped" 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." @@ -457,10 +461,10 @@ msgstr "" msgid "Upgrade all installed packages" msgstr "" -#: upgrade.go:83 +#: upgrade.go:90 msgid "Error checking for updates" msgstr "" -#: upgrade.go:94 +#: upgrade.go:112 msgid "There is nothing to do." msgstr "" diff --git a/internal/translations/po/ru/default.po b/internal/translations/po/ru/default.po index 3d5e548..7f6d85d 100644 --- a/internal/translations/po/ru/default.po +++ b/internal/translations/po/ru/default.po @@ -16,40 +16,57 @@ msgstr "" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "X-Generator: Gtranslator 47.1\n" -#: build.go:41 +#: build.go:44 msgid "Build a local package" msgstr "Сборка локального пакета" -#: build.go:47 +#: build.go:50 msgid "Path to the build script" msgstr "Путь к скрипту сборки" -#: build.go:52 +#: build.go:55 +msgid "Specify package in script (for multi package script only)" +msgstr "" + +#: build.go:60 msgid "Name of the package to build and its repo (example: default/go-bin)" msgstr "Имя пакета для сборки и его репозиторий (пример: default/go-bin)" -#: build.go:57 +#: build.go:65 msgid "" "Build package from scratch even if there's an already built package available" msgstr "Создайте пакет с нуля, даже если уже имеется готовый пакет" -#: build.go:87 +#: build.go:75 +msgid "Error db init" +msgstr "" + +#: build.go:105 +msgid "Package not found" +msgstr "" + +#: build.go:123 msgid "Error pulling repositories" msgstr "Ошибка при извлечении репозиториев" -#: build.go:95 +#: build.go:131 msgid "Unable to detect a supported package manager on the system" msgstr "Не удалось обнаружить поддерживаемый менеджер пакетов в системе" -#: build.go:107 +#: build.go:137 +#, fuzzy +msgid "Error parsing os release" +msgstr "Ошибка при разборе файла выпуска операционной системы" + +#: build.go:158 msgid "Error building package" msgstr "Ошибка при сборке пакета" -#: build.go:114 +#: build.go:165 msgid "Error getting working directory" msgstr "Ошибка при получении рабочего каталога" -#: build.go:123 +#: build.go:174 msgid "Error moving the package" msgstr "Ошибка при перемещении пакета" @@ -137,31 +154,31 @@ msgstr "Ошибка устранения переорпеделений" msgid "Error encoding script variables" msgstr "Ошибка кодирования переменных скрита" -#: install.go:42 +#: install.go:43 msgid "Install a new package" msgstr "Установить новый пакет" -#: install.go:56 +#: install.go:57 msgid "Command install expected at least 1 argument, got %d" msgstr "Для команды install ожидался хотя бы 1 аргумент, получено %d" -#: install.go:91 +#: install.go:124 msgid "Error getting packages" msgstr "Ошибка при получении пакетов" -#: install.go:100 +#: install.go:133 msgid "Error iterating over packages" msgstr "Ошибка при переборе пакетов" -#: install.go:113 +#: install.go:146 msgid "Remove an installed package" msgstr "Удалить установленный пакет" -#: install.go:118 +#: install.go:151 msgid "Command remove expected at least 1 argument, got %d" msgstr "Для команды remove ожидался хотя бы 1 аргумент, получено %d" -#: install.go:130 +#: install.go:163 msgid "Error removing packages" msgstr "Ошибка при удалении пакетов" @@ -233,11 +250,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 +324,27 @@ msgstr "" msgid "Error while running app" msgstr "Ошибка при запуске приложения" -#: pkg/build/build.go:108 +#: pkg/build/build.go:153 msgid "Failed to prompt user to view build script" msgstr "Не удалось предложить пользователю просмотреть скрипт сборки" -#: pkg/build/build.go:112 +#: pkg/build/build.go:157 msgid "Building package" msgstr "Сборка пакета" -#: pkg/build/build.go:156 +#: pkg/build/build.go:228 msgid "Downloading sources" msgstr "Скачивание источников" -#: pkg/build/build.go:168 +#: pkg/build/build.go:246 msgid "Building package metadata" msgstr "Сборка метаданных пакета" -#: pkg/build/build.go:190 +#: pkg/build/build.go:268 msgid "Compressing package" msgstr "Сжатие пакета" -#: pkg/build/build.go:316 +#: pkg/build/build.go:419 msgid "" "Your system's CPU architecture doesn't match this package. Do you want to " "build anyway?" @@ -335,59 +352,46 @@ msgstr "" "Архитектура процессора вашей системы не соответствует этому пакету. Вы все " "равно хотите выполнить сборку?" -#: pkg/build/build.go:327 +#: pkg/build/build.go:433 msgid "This package is already installed" msgstr "Этот пакет уже установлен" -#: pkg/build/build.go:355 +#: pkg/build/build.go:457 msgid "Installing build dependencies" msgstr "Установка зависимостей сборки" -#: pkg/build/build.go:397 +#: pkg/build/build.go:498 msgid "Installing dependencies" msgstr "Установка зависимостей" -#: pkg/build/build.go:443 -msgid "Executing version()" -msgstr "Исполнение версия()" +#: pkg/build/build.go:533 +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 "Исполнение prepare()" - -#: pkg/build/build.go:478 -msgid "Executing build()" -msgstr "Исполнение build()" - -#: pkg/build/build.go:488 -msgid "Executing package()" -msgstr "Исполнение package()" - -#: pkg/build/build.go:510 -msgid "Executing files()" -msgstr "Исполнение files()" - -#: pkg/build/build.go:588 -msgid "AutoProv is not implemented for this package format, so it's skipped" -msgstr "" -"AutoProv не реализовано для этого формата пакета, поэтому будет пропущено" - -#: pkg/build/build.go:599 -msgid "AutoReq is not implemented for this package format, so it's skipped" -msgstr "" -"AutoReq не реализовано для этого формата пакета, поэтому будет пропущено" - -#: pkg/build/build.go:706 +#: pkg/build/build.go:584 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/build.go:647 +msgid "Executing prepare()" +msgstr "Исполнение prepare()" + +#: pkg/build/build.go:657 +msgid "Executing build()" +msgstr "Исполнение build()" + +#: pkg/build/build.go:687 pkg/build/build.go:707 +#, fuzzy +msgid "Executing %s()" +msgstr "Исполнение files()" + +#: pkg/build/build.go:766 +msgid "Error installing native packages" +msgstr "Ошибка при установке нативных пакетов" + +#: pkg/build/build.go:790 +msgid "Error installing package" +msgstr "Ошибка при установке пакета" #: pkg/build/findDeps.go:35 msgid "Command not found on the system" @@ -401,27 +405,29 @@ msgstr "Найденная предоставленная зависимость msgid "Required dependency found" msgstr "Найдена требуемая зависимость" -#: pkg/build/install.go:42 -msgid "Error installing native packages" -msgstr "Ошибка при установке нативных пакетов" +#: pkg/build/utils.go:133 +msgid "AutoProv is not implemented for this package format, so it's skipped" +msgstr "" +"AutoProv не реализовано для этого формата пакета, поэтому будет пропущено" -#: pkg/build/install.go:79 -msgid "Error installing package" -msgstr "Ошибка при установке пакета" +#: pkg/build/utils.go:144 +msgid "AutoReq is not implemented for this package format, so it's skipped" +msgstr "" +"AutoReq не реализовано для этого формата пакета, поэтому будет пропущено" -#: 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." @@ -477,10 +483,19 @@ msgstr "Скачать все изменённые репозитории" msgid "Upgrade all installed packages" msgstr "Обновить все установленные пакеты" -#: upgrade.go:83 +#: upgrade.go:90 msgid "Error checking for updates" msgstr "Ошибка при проверке обновлений" -#: upgrade.go:94 +#: upgrade.go:112 msgid "There is nothing to do." msgstr "Здесь нечего делать." + +#~ msgid "Executing version()" +#~ msgstr "Исполнение версия()" + +#~ msgid "Updating version" +#~ msgstr "Обновление версии" + +#~ msgid "Executing package()" +#~ msgstr "Исполнение package()" diff --git a/internal/types/build.go b/internal/types/build.go index cef93a0..a975e92 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 + Packages []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..0ecd9b3 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -23,113 +23,160 @@ 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" "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" - "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 PackageFinder interface { + FindPkgs(ctx context.Context, pkgs []string) (map[string][]db.Package, []string, error) +} - info, err := distro.ParseOSRelease(ctx) - if err != nil { - return nil, nil, err +type Config interface { + GetPaths(ctx context.Context) *config.Paths + PagerStyle(ctx context.Context) string +} + +type Builder struct { + ctx context.Context + opts types.BuildOpts + info *distro.OSRelease + repos PackageFinder + config Config +} + +func NewBuilder( + ctx context.Context, + opts types.BuildOpts, + repos PackageFinder, + info *distro.OSRelease, + config Config, +) *Builder { + return &Builder{ + ctx: ctx, + opts: opts, + info: info, + repos: repos, + config: config, } +} - fl, err := parseScript(info, opts.Script) +func (b *Builder) UpdateOptsFromPkg(pkg *db.Package, packages []string) { + repodir := b.config.GetPaths(b.ctx).RepoDir + if pkg.BasePkgName != "" { + b.opts.Script = filepath.Join(repodir, pkg.Repository, pkg.BasePkgName, "alr.sh") + b.opts.Packages = packages + } else { + b.opts.Script = filepath.Join(repodir, pkg.Repository, pkg.Name, "alr.sh") + } +} + +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) + basePkg, varsOfPackages, err := b.executeFirstPass(fl) if err != nil { return nil, nil, err } - dirs := getDirs(ctx, vars, opts.Script) + dirs := b.getDirs(basePkg) + + builtPaths := make([]string, 0) // Если флаг opts.Clean не установлен, и пакет уже собран, // возвращаем его, а не собираем заново. - if !opts.Clean { - builtPkgPath, ok, err := checkForBuiltPackage(opts.Manager, vars, getPkgFormat(opts.Manager), dirs.BaseDir) - if err != nil { - return nil, nil, err + if !b.opts.Clean { + var remainingVars []*types.BuildVars + + for _, vars := range varsOfPackages { + builtPkgPath, ok, err := checkForBuiltPackage( + b.opts.Manager, + vars, + getPkgFormat(b.opts.Manager), + dirs.BaseDir, + b.info, + ) + if err != nil { + return nil, nil, err + } + + if ok { + builtPaths = append(builtPaths, builtPkgPath) + } else { + remainingVars = append(remainingVars, vars) + } } - if ok { - return []string{builtPkgPath}, nil, err + if len(remainingVars) == 0 { + return builtPaths, nil, nil } } // Спрашиваем у пользователя, хочет ли он увидеть скрипт сборки. - err = cliutils.PromptViewScript(ctx, opts.Script, vars.Name, config.Config(ctx).PagerStyle, opts.Interactive) + err = cliutils.PromptViewScript( + ctx, + b.opts.Script, + basePkg, + 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) } - slog.Info(gotext.Get("Building package"), "name", vars.Name, "version", vars.Version) + slog.Info(gotext.Get("Building package"), "name", basePkg) // Второй проход будет использоваться для выполнения реального кода, // поэтому он не ограничен. Скрипт уже был показан // пользователю к этому моменту, так что это должно быть безопасно. - 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) // Выполняем различные проверки - if err != nil { - return nil, nil, err - } else if !cont { - os.Exit(1) // Если проверки не пройдены, выходим из программы + for _, vars := range varsOfPackages { + cont, err := b.performChecks(ctx, vars, installed) // Выполняем различные проверки + if err != nil { + return nil, nil, err + } else if !cont { + os.Exit(1) // Если проверки не пройдены, выходим из программы + } } // Подготавливаем директории для сборки @@ -138,72 +185,104 @@ func BuildPackage(ctx context.Context, opts types.BuildOpts) ([]string, []string return nil, nil, err } - buildDeps, err := installBuildDeps(ctx, reposInstance, vars, opts) // Устанавливаем зависимости для сборки + buildDepends := []string{} + optDepends := []string{} + depends := []string{} + sources := []string{} + checksums := []string{} + for _, vars := range varsOfPackages { + buildDepends = append(buildDepends, vars.BuildDepends...) + optDepends = append(optDepends, vars.OptDepends...) + depends = append(depends, vars.Depends...) + sources = append(sources, vars.Sources...) + checksums = append(checksums, vars.Checksums...) + } + buildDepends = removeDuplicates(buildDepends) + optDepends = removeDuplicates(optDepends) + depends = removeDuplicates(depends) + sources = removeDuplicates(sources) + checksums = removeDuplicates(checksums) + + mergedVars := types.BuildVars{ + Sources: sources, + Checksums: checksums, + } + + buildDeps, err := b.installBuildDeps(ctx, buildDepends) // Устанавливаем зависимости для сборки if err != nil { return nil, nil, err } - err = installOptDeps(ctx, reposInstance, vars, opts) // Устанавливаем опциональные зависимости + err = b.installOptDeps(ctx, optDepends) // Устанавливаем опциональные зависимости if err != nil { return nil, nil, err } - builtPaths, builtNames, repoDeps, err := buildALRDeps(ctx, opts, vars) // Собираем зависимости + newBuildPaths, builtNames, repoDeps, err := b.buildALRDeps(ctx, depends) // Собираем зависимости if err != nil { return nil, nil, err } + builtPaths = append(builtPaths, newBuildPaths...) + slog.Info(gotext.Get("Downloading sources")) // Записываем в лог загрузку источников - err = getSources(ctx, dirs, vars) // Загружаем исходники + err = b.getSources(ctx, dirs, &mergedVars) // Загружаем исходники if err != nil { return nil, nil, err } - funcOut, err := executeFunctions(ctx, dec, dirs, vars) // Выполняем специальные функции + err = b.executeFunctions(ctx, dec, dirs) // Выполняем специальные функции if err != nil { return nil, nil, err } - slog.Info(gotext.Get("Building package metadata"), "name", vars.Name) + for _, vars := range varsOfPackages { + funcOut, err := b.executePackageFunctions(ctx, dec, dirs, vars.Name) + if err != nil { + return nil, nil, err + } - pkgFormat := getPkgFormat(opts.Manager) // Получаем формат пакета + slog.Info(gotext.Get("Building package metadata"), "name", basePkg) - pkgInfo, err := buildPkgMetadata(ctx, vars, dirs, pkgFormat, info, append(repoDeps, builtNames...), funcOut.Contents) // Собираем метаданные пакета + pkgFormat := getPkgFormat(b.opts.Manager) // Получаем формат пакета + + pkgInfo, err := buildPkgMetadata(ctx, vars, dirs, pkgFormat, b.info, append(repoDeps, builtNames...), funcOut.Contents) // Собираем метаданные пакета + if err != nil { + return nil, nil, err + } + + packager, err := nfpm.Get(pkgFormat) // Получаем упаковщик для формата пакета + if err != nil { + return nil, nil, err + } + + pkgName := packager.ConventionalFileName(pkgInfo) // Получаем имя файла пакета + pkgPath := filepath.Join(dirs.BaseDir, pkgName) // Определяем путь к пакету + + pkgFile, err := os.Create(pkgPath) // Создаём файл пакета + if err != nil { + return nil, nil, err + } + + slog.Info(gotext.Get("Compressing package"), "name", pkgName) // Логгируем сжатие пакета + + err = packager.Package(pkgInfo, pkgFile) // Упаковываем пакет + if err != nil { + return nil, nil, err + } + + // Добавляем путь и имя только что собранного пакета в + // соответствующие срезы + builtPaths = append(builtPaths, pkgPath) + builtNames = append(builtNames, vars.Name) + } + + err = b.removeBuildDeps(ctx, buildDeps) // Удаляем зависимости для сборки if err != nil { return nil, nil, err } - packager, err := nfpm.Get(pkgFormat) // Получаем упаковщик для формата пакета - if err != nil { - return nil, nil, err - } - - pkgName := packager.ConventionalFileName(pkgInfo) // Получаем имя файла пакета - pkgPath := filepath.Join(dirs.BaseDir, pkgName) // Определяем путь к пакету - - pkgFile, err := os.Create(pkgPath) // Создаём файл пакета - if err != nil { - return nil, nil, err - } - - slog.Info(gotext.Get("Compressing package"), "name", pkgName) // Логгируем сжатие пакета - - err = packager.Package(pkgInfo, pkgFile) // Упаковываем пакет - if err != nil { - return nil, nil, err - } - - err = removeBuildDeps(ctx, buildDeps, opts) // Удаляем зависимости для сборки - if err != nil { - return nil, nil, err - } - - // Добавляем путь и имя только что собранного пакета в - // соответствующие срезы - builtPaths = append(builtPaths, pkgPath) - builtNames = append(builtNames, vars.Name) - // Удаляем дубликаты из pkgPaths и pkgNames. // Дубликаты могут появиться, если несколько зависимостей // зависят от одних и тех же пакетов. @@ -213,27 +292,15 @@ 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, +) (string, []*types.BuildVars, error) { + varsOfPackages := []*types.BuildVars{} + + scriptDir := filepath.Dir(b.opts.Script) // Получаем директорию скрипта + env := createBuildEnvVars(b.info, types.Directories{ScriptDir: scriptDir}) // Создаём переменные окружения для сборки runner, err := interp.New( interp.Env(expand.ListEnviron(env...)), // Устанавливаем окружение @@ -244,40 +311,87 @@ func executeFirstPass(ctx context.Context, info *distro.OSRelease, fl *syntax.Fi interp.OpenHandler(handlers.RestrictedOpen(scriptDir)), // Ограничиваем открытие файлов ) if err != nil { - return nil, err + return "", nil, err } - err = runner.Run(ctx, fl) // Запускаем скрипт + err = runner.Run(b.ctx, fl) // Запускаем скрипт if err != nil { - return nil, err + return "", nil, err } - dec := decoder.New(info, runner) // Создаём новый декодер + dec := decoder.New(b.info, runner) // Создаём новый декодер + type packages struct { + BasePkgName string `sh:"basepkg_name"` + Names []string `sh:"name"` + } + + var pkgs packages + err = dec.DecodeVars(&pkgs) + if err != nil { + return "", nil, err + } + if len(pkgs.Names) == 0 { + return "", nil, errors.New("package name is missing") + } var vars types.BuildVars - err = dec.DecodeVars(&vars) // Декодируем переменные - if err != nil { - return nil, err + if len(pkgs.Names) == 1 { + err = dec.DecodeVars(&vars) // Декодируем переменные + if err != nil { + return "", nil, err + } + varsOfPackages = append(varsOfPackages, &vars) + + return vars.Name, varsOfPackages, nil + } + if len(b.opts.Packages) == 0 { + return "", nil, errors.New("script has multiple packages but package is not specified") } - return &vars, nil // Возвращаем переменные сборки + for _, pkgName := range b.opts.Packages { + var preVars types.BuildVarsPre + funcName := fmt.Sprintf("meta_%s", pkgName) + meta, ok := dec.GetFuncWithSubshell(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 = pkgName + + varsOfPackages = append(varsOfPackages, &vars) + } + + return pkgs.BasePkgName, varsOfPackages, 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(basePkg string) types.Directories { + baseDir := filepath.Join(b.config.GetPaths(b.ctx).PkgsDir, basePkg) // Определяем базовую директорию 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 +408,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,84 +439,76 @@ 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, buildDepends []string) ([]string, error) { var buildDeps []string - if len(vars.BuildDepends) > 0 { - deps, err := removeAlreadyInstalled(opts, vars.BuildDepends) + if len(buildDepends) > 0 { + deps, err := removeAlreadyInstalled(b.opts, buildDepends) if err != nil { return nil, err } - found, notFound, err := repos.FindPkgs(ctx, deps) // Находим пакеты-зависимости + found, notFound, err := b.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) // Уплощаем список зависимостей + flattened := cliutils.FlattenPkgs(ctx, found, "install", b.opts.Interactive) // Уплощаем список зависимостей buildDeps = packageNames(flattened) - InstallPkgs(ctx, flattened, notFound, opts) // Устанавливаем пакеты + b.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 +func (b *Builder) getBuildersForPackages(pkgs []db.Package) []*Builder { + type item struct { + pkg *db.Package + packages []string } - if len(optDeps) > 0 { - optDeps, err := cliutils.ChooseOptDepends(ctx, optDeps, "install", opts.Interactive) // Пользователя просят выбрать опциональные зависимости - if err != nil { - return err + pkgsMap := make(map[string]*item) + for _, pkg := range pkgs { + if pkgsMap[pkg.BasePkgName] == nil { + pkgsMap[pkg.BasePkgName] = &item{ + pkg: &pkg, + } } - - 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) // Устанавливаем выбранные пакеты + pkgsMap[pkg.BasePkgName].packages = append( + pkgsMap[pkg.BasePkgName].packages, + pkg.Name, + ) } - return nil + + builders := []*Builder{} + + for basePkgName := range pkgsMap { + pkg := pkgsMap[basePkgName].pkg + builder := *b + builder.UpdateOptsFromPkg(pkg, pkgsMap[basePkgName].packages) + builders = append(builders, &builder) + } + + return builders } -// Функция buildALRDeps собирает все ALR зависимости пакета. Возвращает пути и имена -// пакетов, которые она собрала, а также все зависимости, которые не были найдены в ALR репозитории, -// чтобы они могли быть установлены из системных репозиториев. -func buildALRDeps(ctx context.Context, opts types.BuildOpts, vars *types.BuildVars) (builtPaths, builtNames, repoDeps []string, err error) { - if len(vars.Depends) > 0 { +func (b *Builder) buildALRDeps(ctx context.Context, depends []string) (builtPaths, builtNames, repoDeps []string, err error) { + if len(depends) > 0 { slog.Info(gotext.Get("Installing dependencies")) - found, notFound, err := repos.FindPkgs(ctx, vars.Depends) // Поиск зависимостей + found, notFound, err := b.repos.FindPkgs(ctx, depends) // Поиск зависимостей if err != nil { return nil, nil, nil, err } 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) + builders := b.getBuildersForPackages(pkgs) + for _, builder := range builders { // Собираем зависимости - pkgPaths, pkgNames, err := BuildPackage(ctx, newOpts) + pkgPaths, pkgNames, err := builder.BuildPackage(ctx) if err != nil { return nil, nil, nil, err } @@ -419,8 +517,6 @@ func buildALRDeps(ctx context.Context, opts types.BuildOpts, vars *types.BuildVa builtPaths = append(builtPaths, pkgPaths...) // Добавляем пути всех собранных пакетов в builtPaths builtNames = append(builtNames, pkgNames...) - // Добавляем имя текущего пакета в builtNames - builtNames = append(builtNames, filepath.Base(filepath.Dir(script))) } } @@ -432,382 +528,7 @@ func buildALRDeps(ctx context.Context, opts types.BuildOpts, vars *types.BuildVa return builtPaths, builtNames, repoDeps, 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) { - version, ok := dec.GetFunc("version") - if ok { - slog.Info(gotext.Get("Executing version()")) - - buf := &bytes.Buffer{} - - err := version( - ctx, - interp.Dir(dirs.SrcDir), - interp.StdIO(os.Stdin, buf, os.Stderr), - ) - if err != nil { - return nil, err - } - - newVer := strings.TrimSpace(buf.String()) - err = setVersion(ctx, dec.Runner, newVer) - if err != nil { - return nil, err - } - vars.Version = newVer - - slog.Info(gotext.Get("Updating version"), "new", newVer) - } - - prepare, ok := dec.GetFunc("prepare") - if ok { - slog.Info(gotext.Get("Executing prepare()")) - - err := prepare(ctx, interp.Dir(dirs.SrcDir)) - if err != nil { - return nil, err - } - } - - build, ok := dec.GetFunc("build") - if ok { - slog.Info(gotext.Get("Executing build()")) - - err := build(ctx, interp.Dir(dirs.SrcDir)) - if err != nil { - return nil, err - } - } - - packageFn, ok := dec.GetFunc("package") - if ok { - slog.Info(gotext.Get("Executing package()")) - err := packageFn(ctx, interp.Dir(dirs.SrcDir)) - if err != nil { - return nil, err - } - } - - output := &FunctionsOutput{} - - files, ok := dec.GetFuncP("files", 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 - // - https://github.com/mvdan/sh/issues/1125 - script, err := syntax.NewParser().Parse(strings.NewReader("cd $pkgdir && shopt -s globstar"), "") - if err != nil { - return err - } - return s.Run(ctx, script) - }) - - if ok { - slog.Info(gotext.Get("Executing files()")) - - buf := &bytes.Buffer{} - - err := files( - ctx, - interp.Dir(dirs.PkgDir), - interp.StdIO(os.Stdin, buf, os.Stderr), - ) - if err != nil { - return nil, err - } - - contents, err := shlex.Split(buf.String()) - if err != nil { - return nil, err - } - output.Contents = &contents - } - - 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 { +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) @@ -843,8 +564,7 @@ func getSources(ctx context.Context, dirs types.Directories, bv *types.BuildVars } } - cfg := config.GetInstance(ctx) - opts.DlCache = dlcache.New(cfg) + opts.DlCache = dlcache.New(b.config) err := dl.Download(ctx, opts) if err != nil { @@ -855,91 +575,221 @@ func getSources(ctx context.Context, dirs types.Directories, bv *types.BuildVars 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) - } +// Функция 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 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) + if remove { + err = b.opts.Manager.Remove( + &manager.Opts{ + AsRoot: true, + NoConfirm: true, + }, + buildDeps..., + ) + if err != nil { + return err + } + } } + return nil } -// Функция 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) +type FunctionsOutput struct { + Contents *[]string } -// Returns not installed dependencies -func removeAlreadyInstalled(opts types.BuildOpts, dependencies []string) ([]string, error) { - filteredPackages := []string{} +// Функция executeFunctions выполняет специальные функции ALR, такие как version(), prepare() и т.д. +func (b *Builder) executeFunctions( + ctx context.Context, + dec *decoder.Decoder, + dirs types.Directories, +) error { + /* + version, ok := dec.GetFunc("version") + if ok { + slog.Info(gotext.Get("Executing version()")) - for _, dep := range dependencies { - installed, err := opts.Manager.IsInstalled(dep) + buf := &bytes.Buffer{} + + err := version( + ctx, + interp.Dir(dirs.SrcDir), + interp.StdIO(os.Stdin, buf, os.Stderr), + ) + if err != nil { + return nil, err + } + + newVer := strings.TrimSpace(buf.String()) + err = setVersion(ctx, dec.Runner, newVer) + if err != nil { + return nil, err + } + vars.Version = newVer + + slog.Info(gotext.Get("Updating version"), "new", newVer) + } + */ + + prepare, ok := dec.GetFunc("prepare") + if ok { + slog.Info(gotext.Get("Executing prepare()")) + + err := prepare(ctx, interp.Dir(dirs.SrcDir)) + if err != nil { + return err + } + } + + build, ok := dec.GetFunc("build") + if ok { + slog.Info(gotext.Get("Executing build()")) + + err := build(ctx, interp.Dir(dirs.SrcDir)) + if err != nil { + return err + } + } + + return nil +} + +func (b *Builder) executePackageFunctions( + ctx context.Context, + dec *decoder.Decoder, + dirs types.Directories, + packageName string, +) (*FunctionsOutput, error) { + output := &FunctionsOutput{} + var packageFuncName string + var filesFuncName string + + if packageName == "" { + filesFuncName = "files" + packageFuncName = "package" + } else { + packageFuncName = fmt.Sprintf("package_%s", packageName) + filesFuncName = fmt.Sprintf("files_%s", packageName) + } + packageFn, ok := dec.GetFunc(packageFuncName) + if ok { + slog.Info(gotext.Get("Executing %s()", packageFuncName)) + err := packageFn(ctx, interp.Dir(dirs.SrcDir)) if err != nil { return nil, err } - if installed { - continue + } + + 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 + // - https://github.com/mvdan/sh/issues/1125 + script, err := syntax.NewParser().Parse(strings.NewReader("cd $pkgdir && shopt -s globstar"), "") + if err != nil { + return err } - filteredPackages = append(filteredPackages, dep) + return s.Run(ctx, script) + }) + + if ok { + slog.Info(gotext.Get("Executing %s()", filesFuncName)) + + buf := &bytes.Buffer{} + + err := files( + ctx, + interp.Dir(dirs.PkgDir), + interp.StdIO(os.Stdin, buf, os.Stderr), + ) + if err != nil { + return nil, err + } + + contents, err := shlex.Split(buf.String()) + if err != nil { + return nil, err + } + output.Contents = &contents } - return filteredPackages, nil + return output, nil } -// Функция packageNames возвращает имена всех предоставленных пакетов. -func packageNames(pkgs []db.Package) []string { - names := make([]string, len(pkgs)) - for i, p := range pkgs { - names[i] = p.Name +func (b *Builder) installOptDeps(ctx context.Context, optDepends []string) error { + optDeps, err := removeAlreadyInstalled(b.opts, optDepends) + if err != nil { + return err } - return names + if len(optDeps) > 0 { + optDeps, err := cliutils.ChooseOptDepends(ctx, optDeps, "install", b.opts.Interactive) // Пользователя просят выбрать опциональные зависимости + if err != nil { + return err + } + + if len(optDeps) == 0 { + return nil + } + + found, notFound, err := b.repos.FindPkgs(ctx, optDeps) // Находим опциональные зависимости + if err != nil { + return err + } + + flattened := cliutils.FlattenPkgs(ctx, found, "install", b.opts.Interactive) + b.InstallPkgs(ctx, flattened, notFound, b.opts) // Устанавливаем выбранные пакеты + } + return nil } -// Функция 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) +func (b *Builder) InstallPkgs( + ctx context.Context, + alrPkgs []db.Package, + nativePkgs []string, + opts types.BuildOpts, +) { + if len(nativePkgs) > 0 { + err := opts.Manager.Install(nil, nativePkgs...) + // Если есть нативные пакеты, выполняем их установку + if err != nil { + slog.Error(gotext.Get("Error installing native packages"), "err", err) + os.Exit(1) + // Логируем и завершаем выполнение при ошибке } } - return result + b.InstallALRPackages(ctx, alrPkgs, opts) + // Устанавливаем скрипты сборки через функцию InstallScripts +} + +func (b *Builder) InstallALRPackages(ctx context.Context, pkgs []db.Package, opts types.BuildOpts) { + builders := b.getBuildersForPackages(pkgs) + for _, builder := range builders { + builtPkgs, _, err := builder.BuildPackage(ctx) + // Выполняем сборку пакета + if err != nil { + slog.Error(gotext.Get("Error building package"), "err", err) + os.Exit(1) + // Логируем и завершаем выполнение при ошибке сборки + } + + err = opts.Manager.InstallLocal(nil, builtPkgs...) + // Устанавливаем локально собранные пакеты + if err != nil { + slog.Error(gotext.Get("Error installing package"), "err", err) + os.Exit(1) + // Логируем и завершаем выполнение при ошибке установки + } + } } diff --git a/pkg/build/build_internal_test.go b/pkg/build/build_internal_test.go index e09fac0..10fd80c 100644 --- a/pkg/build/build_internal_test.go +++ b/pkg/build/build_internal_test.go @@ -18,10 +18,18 @@ package build import ( "context" + "fmt" + "os" + "strings" "testing" + "github.com/stretchr/testify/assert" + "mvdan.cc/sh/v3/syntax" + + "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/manager" ) @@ -134,93 +142,145 @@ func (m *TestManager) IsInstalled(pkg string) (bool, error) { return true, nil } -// TODO: fix test -func TestInstallBuildDeps(t *testing.T) { - type testEnv struct { - pf PackageFinder - vars *types.BuildVars - opts types.BuildOpts +type TestConfig struct{} - // Contains pkgs captured by FindPkgsFunc - // capturedPkgs []string +func (c *TestConfig) PagerStyle(ctx context.Context) string { + return "native" +} + +func (c *TestConfig) GetPaths(ctx context.Context) *config.Paths { + return &config.Paths{ + CacheDir: "/tmp", + } +} + +func TestExecuteFirstPassIsSecure(t *testing.T) { + cfg := &TestConfig{} + pf := &TestPackageFinder{} + info := &distro.OSRelease{} + m := &TestManager{} + + opts := types.BuildOpts{ + Manager: m, + Interactive: false, } + ctx := context.Background() + + b := NewBuilder( + ctx, + opts, + pf, + info, + cfg, + ) + + tmpFile, err := os.CreateTemp("", "testfile-") + assert.NoError(t, err) + tmpFilePath := tmpFile.Name() + defer os.Remove(tmpFilePath) + + _, err = os.Stat(tmpFilePath) + assert.NoError(t, err) + + testScript := fmt.Sprintf(`name='test' +version=1.0.0 +release=1 +rm -f %s`, tmpFilePath) + + fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "alr.sh") + assert.NoError(t, err) + + _, _, err = b.executeFirstPass(fl) + assert.NoError(t, err) + + _, err = os.Stat(tmpFilePath) + assert.NoError(t, err) +} + +func TestExecuteFirstPassIsCorrect(t *testing.T) { type testCase struct { Name string - Prepare func() *testEnv - Expected func(t *testing.T, e *testEnv, res []string, err error) + Script string + Opts types.BuildOpts + Expected func(t *testing.T, vars []*types.BuildVars) } - for _, tc := range []testCase{ - /* - { - Name: "install only needed deps", - Prepare: func() *testEnv { - pf := TestPackageFinder{} - vars := types.BuildVars{} - m := TestManager{} - opts := types.BuildOpts{ - Manager: &m, - Interactive: false, - } + for _, tc := range []testCase{{ + Name: "single package", + Script: `name='test' +version='1.0.0' +release=1 +epoch=2 +desc="Test package" +homepage='https://example.com' +maintainer='Ivan Ivanov' +`, + Opts: types.BuildOpts{ + Manager: &TestManager{}, + Interactive: false, + }, + Expected: func(t *testing.T, vars []*types.BuildVars) { + assert.Equal(t, 1, len(vars)) + assert.Equal(t, vars[0].Name, "test") + assert.Equal(t, vars[0].Version, "1.0.0") + assert.Equal(t, vars[0].Release, int(1)) + assert.Equal(t, vars[0].Epoch, uint(2)) + assert.Equal(t, vars[0].Description, "Test package") + }, + }, { + Name: "multiple packages", + Script: `name=( + foo + bar +) - env := &testEnv{ - pf: &pf, - vars: &vars, - opts: opts, - capturedPkgs: []string{}, - } +version='0.0.1' +release=1 +epoch=2 +desc="Test package" - pf.FindPkgsFunc = func(ctx context.Context, pkgs []string) (map[string][]db.Package, []string, error) { - env.capturedPkgs = append(env.capturedPkgs, pkgs...) - result := make(map[string][]db.Package) - result["bar"] = []db.Package{{ - Name: "bar-pkg", - }} - result["buz"] = []db.Package{{ - Name: "buz-pkg", - }} +meta_foo() { + desc="Foo package" +} - return result, []string{}, nil - } +meta_bar() { - vars.BuildDepends = []string{ - "foo", - "bar", - "buz", - } - m.IsInstalledFunc = func(pkg string) (bool, error) { - if pkg == "foo" { - return true, nil - } else { - return false, nil - } - } +} +`, + Opts: types.BuildOpts{ + Packages: []string{"foo"}, + Manager: &TestManager{}, + Interactive: false, + }, + Expected: func(t *testing.T, vars []*types.BuildVars) { + assert.Equal(t, 1, len(vars)) + assert.Equal(t, vars[0].Name, "foo") + assert.Equal(t, vars[0].Description, "Foo package") + }, + }} { + t.Run(tc.Name, func(t *testing.T) { + cfg := &TestConfig{} + pf := &TestPackageFinder{} + info := &distro.OSRelease{} - return env - }, - Expected: func(t *testing.T, e *testEnv, res []string, err error) { - assert.NoError(t, err) - assert.Len(t, res, 2) - assert.ElementsMatch(t, res, []string{"bar-pkg", "buz-pkg"}) - - assert.ElementsMatch(t, e.capturedPkgs, []string{"bar", "buz"}) - }, - }, - */ - } { - t.Run(tc.Name, func(tt *testing.T) { ctx := context.Background() - env := tc.Prepare() - result, err := installBuildDeps( + b := NewBuilder( ctx, - env.pf, - env.vars, - env.opts, + tc.Opts, + pf, + info, + cfg, ) - tc.Expected(tt, env, result, err) + fl, err := syntax.NewParser().Parse(strings.NewReader(tc.Script), "alr.sh") + assert.NoError(t, err) + + _, allVars, err := b.executeFirstPass(fl) + assert.NoError(t, err) + + tc.Expected(t, allVars) }) } } diff --git a/pkg/build/install.go b/pkg/build/install.go deleted file mode 100644 index 902fb93..0000000 --- a/pkg/build/install.go +++ /dev/null @@ -1,84 +0,0 @@ -// 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" - "log/slog" - "os" - "path/filepath" - - "github.com/leonelquinteros/gotext" - - "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" -) - -// InstallPkgs устанавливает нативные пакеты с использованием менеджера пакетов, -// затем строит и устанавливает пакеты ALR -func InstallPkgs(ctx context.Context, alrPkgs []db.Package, nativePkgs []string, opts types.BuildOpts) { - if len(nativePkgs) > 0 { - err := opts.Manager.Install(nil, nativePkgs...) - // Если есть нативные пакеты, выполняем их установку - if err != nil { - slog.Error(gotext.Get("Error installing native packages"), "err", err) - os.Exit(1) - // Логируем и завершаем выполнение при ошибке - } - } - - InstallScripts(ctx, GetScriptPaths(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) - } - 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) - // Выполняем сборку пакета - if err != nil { - slog.Error(gotext.Get("Error building package"), "err", err) - os.Exit(1) - // Логируем и завершаем выполнение при ошибке сборки - } - - err = opts.Manager.InstallLocal(nil, builtPkgs...) - // Устанавливаем локально собранные пакеты - if err != nil { - slog.Error(gotext.Get("Error installing package"), "err", err) - os.Exit(1) - // Логируем и завершаем выполнение при ошибке установки - } - } -} diff --git a/pkg/build/utils.go b/pkg/build/utils.go new file mode 100644 index 0000000..3b3ba49 --- /dev/null +++ b/pkg/build/utils.go @@ -0,0 +1,423 @@ +// 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/syntax" + + "github.com/goreleaser/nfpm/v2" + "github.com/goreleaser/nfpm/v2/files" + + "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) // Создаем директорию для пакетов +} + +// Функция 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, info) + 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 + }) + } + + 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, + info *distro.OSRelease, +) (string, bool, error) { + filename, err := pkgFileName(vars, pkgFormat, info) + 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, info *distro.OSRelease) *nfpm.Info { + return &nfpm.Info{ + Name: vars.Name, + Arch: cpu.Arch(), + Version: vars.Version, + Release: overrides.ReleasePlatformSpecific(vars.Release, info), + 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, info *distro.OSRelease) (string, error) { + pkgInfo := getBasePkgInfo(vars, info) + + 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/repos/pull.go b/pkg/repos/pull.go index 1d41a52..98a86fd 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.GetFuncWithSubshell(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..81b3bb6 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" ) @@ -36,15 +35,6 @@ func Pull(ctx context.Context, repos []types.Repo) error { return GetInstance(ctx).Pull(ctx, repos) } -// FindPkgs looks for packages matching the inputs inside the database. -// It returns a map that maps the package name input to any packages found for it. -// 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) { - return GetInstance(ctx).FindPkgs(ctx, pkgs) -} - // ======================= // FOR LEGACY ONLY // ======================= 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", diff --git a/scripts/install.sh b/scripts/install.sh index 1fd7a72..c642b4a 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,3 +1,22 @@ +# 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 . + info() { echo $'\x1b[32m[ИНФО]\x1b[0m' $@ } diff --git a/upgrade.go b/upgrade.go index e7b152a..553cf41 100644 --- a/upgrade.go +++ b/upgrade.go @@ -32,7 +32,7 @@ import ( "golang.org/x/exp/slices" "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/overrides" "gitea.plemya-x.ru/Plemya-x/ALR/internal/types" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/build" @@ -56,7 +56,14 @@ func UpgradeCmd() *cli.Command { Action: func(c *cli.Context) error { ctx := c.Context - cfg := config.GetInstance(ctx) + 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) + } info, err := distro.ParseOSRelease(ctx) if err != nil { @@ -71,21 +78,32 @@ func UpgradeCmd() *cli.Command { } if cfg.AutoPull(ctx) { - err = repos.Pull(ctx, config.Config(ctx).Repos) + err = rs.Pull(ctx, cfg.Repos(ctx)) if err != nil { slog.Error(gotext.Get("Error pulling repos"), "err", err) os.Exit(1) } } - updates, err := checkForUpdates(ctx, mgr, info) + updates, err := checkForUpdates(ctx, mgr, cfg, rs, info) if err != nil { slog.Error(gotext.Get("Error checking for updates"), "err", err) os.Exit(1) } if len(updates) > 0 { - build.InstallPkgs(ctx, updates, nil, types.BuildOpts{ + builder := build.NewBuilder( + ctx, + types.BuildOpts{ + Manager: mgr, + Clean: c.Bool("clean"), + Interactive: c.Bool("interactive"), + }, + rs, + info, + cfg, + ) + builder.InstallPkgs(ctx, updates, nil, types.BuildOpts{ Manager: mgr, Clean: c.Bool("clean"), Interactive: c.Bool("interactive"), @@ -99,27 +117,33 @@ func UpgradeCmd() *cli.Command { } } -func checkForUpdates(ctx context.Context, mgr manager.Manager, info *distro.OSRelease) ([]db.Package, error) { +func checkForUpdates( + ctx context.Context, + mgr manager.Manager, + cfg *config.ALRConfig, + rs *repos.Repos, + info *distro.OSRelease, +) ([]database.Package, error) { installed, err := mgr.ListInstalled(nil) if err != nil { return nil, err } pkgNames := maps.Keys(installed) - found, _, err := repos.FindPkgs(ctx, pkgNames) + found, _, err := rs.FindPkgs(ctx, pkgNames) if err != nil { return nil, err } - var out []db.Package + var out []database.Package for pkgName, pkgs := range found { - if slices.Contains(config.Config(ctx).IgnorePkgUpdates, pkgName) { + if slices.Contains(cfg.IgnorePkgUpdates(ctx), pkgName) { continue } if len(pkgs) > 1 { // Puts the element with the highest version first - slices.SortFunc(pkgs, func(a, b db.Package) int { + slices.SortFunc(pkgs, func(a, b database.Package) int { return vercmp.Compare(a.Version, b.Version) }) }