Поддержка APT-RPM и ALT Linux #1
@@ -41,7 +41,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
// Импортируем пакеты для поддержки различных форматов пакетов (APK, DEB, RPM и ARCH).
|
// Импортируем пакеты для поддержки различных форматов пакетов (APK, DEB, RPM и ARCH).
|
||||||
_ "github.com/goreleaser/nfpm/v2/apk"
|
_ "github.com/goreleaser/nfpm/v2/apk"
|
||||||
_ "github.com/goreleaser/nfpm/v2/arch"
|
_ "github.com/goreleaser/nfpm/v2/arch"
|
||||||
_ "github.com/goreleaser/nfpm/v2/deb"
|
_ "github.com/goreleaser/nfpm/v2/deb"
|
||||||
@@ -82,8 +82,8 @@ func BuildPackage(ctx context.Context, opts types.BuildOpts) ([]string, []string
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Первый проход предназначен для получения значений переменных и выполняется
|
// Первый проход предназначен для получения значений переменных и выполняется
|
||||||
// до отображения скрипта, чтобы предотвратить выполнение вредоносного кода.
|
// до отображения скрипта, чтобы предотвратить выполнение вредоносного кода.
|
||||||
vars, err := executeFirstPass(ctx, info, fl, opts.Script)
|
vars, err := executeFirstPass(ctx, info, fl, opts.Script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -91,8 +91,8 @@ func BuildPackage(ctx context.Context, opts types.BuildOpts) ([]string, []string
|
|||||||
|
|
||||||
dirs := getDirs(ctx, vars, opts.Script)
|
dirs := getDirs(ctx, vars, opts.Script)
|
||||||
|
|
||||||
// Если флаг opts.Clean не установлен, и пакет уже собран,
|
// Если флаг opts.Clean не установлен, и пакет уже собран,
|
||||||
// возвращаем его, а не собираем заново.
|
// возвращаем его, а не собираем заново.
|
||||||
if !opts.Clean {
|
if !opts.Clean {
|
||||||
builtPkgPath, ok, err := checkForBuiltPackage(opts.Manager, vars, getPkgFormat(opts.Manager), dirs.BaseDir)
|
builtPkgPath, ok, err := checkForBuiltPackage(opts.Manager, vars, getPkgFormat(opts.Manager), dirs.BaseDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -104,7 +104,7 @@ func BuildPackage(ctx context.Context, opts types.BuildOpts) ([]string, []string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Спрашиваем у пользователя, хочет ли он увидеть скрипт сборки.
|
// Спрашиваем у пользователя, хочет ли он увидеть скрипт сборки.
|
||||||
err = cliutils.PromptViewScript(ctx, opts.Script, vars.Name, config.Config(ctx).PagerStyle, opts.Interactive)
|
err = cliutils.PromptViewScript(ctx, opts.Script, vars.Name, config.Config(ctx).PagerStyle, opts.Interactive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Failed to prompt user to view build script").Err(err).Send()
|
log.Fatal("Failed to prompt user to view build script").Err(err).Send()
|
||||||
@@ -112,9 +112,9 @@ func BuildPackage(ctx context.Context, opts types.BuildOpts) ([]string, []string
|
|||||||
|
|
||||||
log.Info("Building package").Str("name", vars.Name).Str("version", vars.Version).Send()
|
log.Info("Building package").Str("name", vars.Name).Str("version", vars.Version).Send()
|
||||||
|
|
||||||
// Второй проход будет использоваться для выполнения реального кода,
|
// Второй проход будет использоваться для выполнения реального кода,
|
||||||
// поэтому он не ограничен. Скрипт уже был показан
|
// поэтому он не ограничен. Скрипт уже был показан
|
||||||
// пользователю к этому моменту, так что это должно быть безопасно.
|
// пользователю к этому моменту, так что это должно быть безопасно.
|
||||||
dec, err := executeSecondPass(ctx, info, fl, dirs)
|
dec, err := executeSecondPass(ctx, info, fl, dirs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -133,7 +133,7 @@ func BuildPackage(ctx context.Context, opts types.BuildOpts) ([]string, []string
|
|||||||
os.Exit(1) // Если проверки не пройдены, выходим из программы
|
os.Exit(1) // Если проверки не пройдены, выходим из программы
|
||||||
}
|
}
|
||||||
|
|
||||||
// Подготавливаем директории для сборки
|
// Подготавливаем директории для сборки
|
||||||
err = prepareDirs(dirs)
|
err = prepareDirs(dirs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -181,7 +181,7 @@ func BuildPackage(ctx context.Context, opts types.BuildOpts) ([]string, []string
|
|||||||
}
|
}
|
||||||
|
|
||||||
pkgName := packager.ConventionalFileName(pkgInfo) // Получаем имя файла пакета
|
pkgName := packager.ConventionalFileName(pkgInfo) // Получаем имя файла пакета
|
||||||
pkgPath := filepath.Join(dirs.BaseDir, pkgName) // Определяем путь к пакету
|
pkgPath := filepath.Join(dirs.BaseDir, pkgName) // Определяем путь к пакету
|
||||||
|
|
||||||
pkgFile, err := os.Create(pkgPath) // Создаём файл пакета
|
pkgFile, err := os.Create(pkgPath) // Создаём файл пакета
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -200,14 +200,14 @@ func BuildPackage(ctx context.Context, opts types.BuildOpts) ([]string, []string
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем путь и имя только что собранного пакета в
|
// Добавляем путь и имя только что собранного пакета в
|
||||||
// соответствующие срезы
|
// соответствующие срезы
|
||||||
pkgPaths := append(builtPaths, pkgPath)
|
pkgPaths := append(builtPaths, pkgPath)
|
||||||
pkgNames := append(builtNames, vars.Name)
|
pkgNames := append(builtNames, vars.Name)
|
||||||
|
|
||||||
// Удаляем дубликаты из pkgPaths и pkgNames.
|
// Удаляем дубликаты из pkgPaths и pkgNames.
|
||||||
// Дубликаты могут появиться, если несколько зависимостей
|
// Дубликаты могут появиться, если несколько зависимостей
|
||||||
// зависят от одних и тех же пакетов.
|
// зависят от одних и тех же пакетов.
|
||||||
pkgPaths = removeDuplicates(pkgPaths)
|
pkgPaths = removeDuplicates(pkgPaths)
|
||||||
pkgNames = removeDuplicates(pkgNames)
|
pkgNames = removeDuplicates(pkgNames)
|
||||||
|
|
||||||
@@ -233,16 +233,16 @@ func parseScript(info *distro.OSRelease, script string) (*syntax.File, error) {
|
|||||||
// Функция executeFirstPass выполняет парсированный скрипт в ограниченной среде,
|
// Функция executeFirstPass выполняет парсированный скрипт в ограниченной среде,
|
||||||
// чтобы извлечь переменные сборки без выполнения реального кода.
|
// чтобы извлечь переменные сборки без выполнения реального кода.
|
||||||
func executeFirstPass(ctx context.Context, info *distro.OSRelease, fl *syntax.File, script string) (*types.BuildVars, error) {
|
func executeFirstPass(ctx context.Context, info *distro.OSRelease, fl *syntax.File, script string) (*types.BuildVars, error) {
|
||||||
scriptDir := filepath.Dir(script) // Получаем директорию скрипта
|
scriptDir := filepath.Dir(script) // Получаем директорию скрипта
|
||||||
env := createBuildEnvVars(info, types.Directories{ScriptDir: scriptDir}) // Создаём переменные окружения для сборки
|
env := createBuildEnvVars(info, types.Directories{ScriptDir: scriptDir}) // Создаём переменные окружения для сборки
|
||||||
|
|
||||||
runner, err := interp.New(
|
runner, err := interp.New(
|
||||||
interp.Env(expand.ListEnviron(env...)), // Устанавливаем окружение
|
interp.Env(expand.ListEnviron(env...)), // Устанавливаем окружение
|
||||||
interp.StdIO(os.Stdin, os.Stdout, os.Stderr), // Устанавливаем стандартный ввод-вывод
|
interp.StdIO(os.Stdin, os.Stdout, os.Stderr), // Устанавливаем стандартный ввод-вывод
|
||||||
interp.ExecHandler(helpers.Restricted.ExecHandler(handlers.NopExec)), // Ограничиваем выполнение
|
interp.ExecHandler(helpers.Restricted.ExecHandler(handlers.NopExec)), // Ограничиваем выполнение
|
||||||
interp.ReadDirHandler(handlers.RestrictedReadDir(scriptDir)), // Ограничиваем чтение директорий
|
interp.ReadDirHandler(handlers.RestrictedReadDir(scriptDir)), // Ограничиваем чтение директорий
|
||||||
interp.StatHandler(handlers.RestrictedStat(scriptDir)), // Ограничиваем доступ к статистике файлов
|
interp.StatHandler(handlers.RestrictedStat(scriptDir)), // Ограничиваем доступ к статистике файлов
|
||||||
interp.OpenHandler(handlers.RestrictedOpen(scriptDir)), // Ограничиваем открытие файлов
|
interp.OpenHandler(handlers.RestrictedOpen(scriptDir)), // Ограничиваем открытие файлов
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -282,8 +282,8 @@ func executeSecondPass(ctx context.Context, info *distro.OSRelease, fl *syntax.F
|
|||||||
|
|
||||||
fakeroot := handlers.FakerootExecHandler(2 * time.Second) // Настраиваем "fakeroot" для выполнения
|
fakeroot := handlers.FakerootExecHandler(2 * time.Second) // Настраиваем "fakeroot" для выполнения
|
||||||
runner, err := interp.New(
|
runner, err := interp.New(
|
||||||
interp.Env(expand.ListEnviron(env...)), // Устанавливаем окружение
|
interp.Env(expand.ListEnviron(env...)), // Устанавливаем окружение
|
||||||
interp.StdIO(os.Stdin, os.Stdout, os.Stderr), // Устанавливаем стандартный ввод-вывод
|
interp.StdIO(os.Stdin, os.Stdout, os.Stderr), // Устанавливаем стандартный ввод-вывод
|
||||||
interp.ExecHandler(helpers.Helpers.ExecHandler(fakeroot)), // Обрабатываем выполнение через fakeroot
|
interp.ExecHandler(helpers.Helpers.ExecHandler(fakeroot)), // Обрабатываем выполнение через fakeroot
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -396,30 +396,30 @@ func buildALRDeps(ctx context.Context, opts types.BuildOpts, vars *types.BuildVa
|
|||||||
}
|
}
|
||||||
repoDeps = notFound
|
repoDeps = notFound
|
||||||
|
|
||||||
// Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез
|
// Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез
|
||||||
pkgs := cliutils.FlattenPkgs(ctx, found, "install", opts.Interactive)
|
pkgs := cliutils.FlattenPkgs(ctx, found, "install", opts.Interactive)
|
||||||
scripts := GetScriptPaths(ctx, pkgs)
|
scripts := GetScriptPaths(ctx, pkgs)
|
||||||
for _, script := range scripts {
|
for _, script := range scripts {
|
||||||
newOpts := opts
|
newOpts := opts
|
||||||
newOpts.Script = script
|
newOpts.Script = script
|
||||||
|
|
||||||
// Собираем зависимости
|
// Собираем зависимости
|
||||||
pkgPaths, pkgNames, err := BuildPackage(ctx, newOpts)
|
pkgPaths, pkgNames, err := BuildPackage(ctx, newOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем пути всех собранных пакетов в builtPaths
|
// Добавляем пути всех собранных пакетов в builtPaths
|
||||||
builtPaths = append(builtPaths, pkgPaths...)
|
builtPaths = append(builtPaths, pkgPaths...)
|
||||||
// Добавляем пути всех собранных пакетов в builtPaths
|
// Добавляем пути всех собранных пакетов в builtPaths
|
||||||
builtNames = append(builtNames, pkgNames...)
|
builtNames = append(builtNames, pkgNames...)
|
||||||
// Добавляем имя текущего пакета в builtNames
|
// Добавляем имя текущего пакета в builtNames
|
||||||
builtNames = append(builtNames, filepath.Base(filepath.Dir(script)))
|
builtNames = append(builtNames, filepath.Base(filepath.Dir(script)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаляем возможные дубликаты, которые могут быть введены, если
|
// Удаляем возможные дубликаты, которые могут быть введены, если
|
||||||
// несколько зависимостей зависят от одних и тех же пакетов.
|
// несколько зависимостей зависят от одних и тех же пакетов.
|
||||||
repoDeps = removeDuplicates(repoDeps)
|
repoDeps = removeDuplicates(repoDeps)
|
||||||
builtPaths = removeDuplicates(builtPaths)
|
builtPaths = removeDuplicates(builtPaths)
|
||||||
builtNames = removeDuplicates(builtNames)
|
builtNames = removeDuplicates(builtNames)
|
||||||
@@ -474,29 +474,29 @@ func executeFunctions(ctx context.Context, dec *decoder.Decoder, dirs types.Dire
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Выполнение всех функций, начинающихся с package_
|
// Выполнение всех функций, начинающихся с package_
|
||||||
for {
|
for {
|
||||||
packageFn, ok := dec.GetFunc("package")
|
packageFn, ok := dec.GetFunc("package")
|
||||||
if ok {
|
if ok {
|
||||||
log.Info("Executing package()").Send()
|
log.Info("Executing package()").Send()
|
||||||
err = packageFn(ctx, interp.Dir(dirs.SrcDir))
|
err = packageFn(ctx, interp.Dir(dirs.SrcDir))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка на наличие дополнительных функций package_*
|
// Проверка на наличие дополнительных функций package_*
|
||||||
packageFuncName := "package_"
|
packageFuncName := "package_"
|
||||||
if packageFunc, ok := dec.GetFunc(packageFuncName); ok {
|
if packageFunc, ok := dec.GetFunc(packageFuncName); ok {
|
||||||
log.Info("Executing " + packageFuncName).Send()
|
log.Info("Executing " + packageFuncName).Send()
|
||||||
err = packageFunc(ctx, interp.Dir(dirs.SrcDir))
|
err = packageFunc(ctx, interp.Dir(dirs.SrcDir))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
break // Если больше нет функций package_*, выходим из цикла
|
break // Если больше нет функций package_*, выходим из цикла
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -517,7 +517,7 @@ func buildPkgMetadata(vars *types.BuildVars, dirs types.Directories, pkgFormat s
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pkgFormat == "apk" {
|
if pkgFormat == "apk" {
|
||||||
// Alpine отказывается устанавливать пакеты, которые предоставляют сами себя, поэтому удаляем такие элементы
|
// Alpine отказывается устанавливать пакеты, которые предоставляют сами себя, поэтому удаляем такие элементы
|
||||||
pkgInfo.Overridables.Provides = slices.DeleteFunc(pkgInfo.Overridables.Provides, func(s string) bool {
|
pkgInfo.Overridables.Provides = slices.DeleteFunc(pkgInfo.Overridables.Provides, func(s string) bool {
|
||||||
return s == pkgInfo.Name
|
return s == pkgInfo.Name
|
||||||
})
|
})
|
||||||
@@ -559,7 +559,7 @@ func buildContents(vars *types.BuildVars, dirs types.Directories) ([]*files.Cont
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если директория пустая, пропускаем её
|
// Если директория пустая, пропускаем её
|
||||||
_, err = f.Readdirnames(1)
|
_, err = f.Readdirnames(1)
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
return nil
|
return nil
|
||||||
@@ -576,13 +576,13 @@ func buildContents(vars *types.BuildVars, dirs types.Directories) ([]*files.Cont
|
|||||||
|
|
||||||
return f.Close()
|
return f.Close()
|
||||||
}
|
}
|
||||||
// Если файл является символической ссылкой, прорабатываем это
|
// Если файл является символической ссылкой, прорабатываем это
|
||||||
if fi.Mode()&os.ModeSymlink != 0 {
|
if fi.Mode()&os.ModeSymlink != 0 {
|
||||||
link, err := os.Readlink(path)
|
link, err := os.Readlink(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Удаляем pkgdir из пути символической ссылки
|
// Удаляем pkgdir из пути символической ссылки
|
||||||
link = strings.TrimPrefix(link, dirs.PkgDir)
|
link = strings.TrimPrefix(link, dirs.PkgDir)
|
||||||
|
|
||||||
contents = append(contents, &files.Content{
|
contents = append(contents, &files.Content{
|
||||||
@@ -597,7 +597,7 @@ func buildContents(vars *types.BuildVars, dirs types.Directories) ([]*files.Cont
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Обрабатываем обычные файлы
|
// Обрабатываем обычные файлы
|
||||||
fileContent := &files.Content{
|
fileContent := &files.Content{
|
||||||
Source: path,
|
Source: path,
|
||||||
Destination: trimmed,
|
Destination: trimmed,
|
||||||
@@ -608,7 +608,7 @@ func buildContents(vars *types.BuildVars, dirs types.Directories) ([]*files.Cont
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если файл должен быть сохранен, установите его тип как config|noreplace
|
// Если файл должен быть сохранен, установите его тип как config|noreplace
|
||||||
if slices.Contains(vars.Backup, trimmed) {
|
if slices.Contains(vars.Backup, trimmed) {
|
||||||
fileContent.Type = "config|noreplace"
|
fileContent.Type = "config|noreplace"
|
||||||
}
|
}
|
||||||
@@ -673,8 +673,8 @@ func getBasePkgInfo(vars *types.BuildVars) *nfpm.Info {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pkgFileName returns the filename of the package if it were to be built.
|
// Функция pkgFileName возвращает имя файла пакета, если оно было бы создано.
|
||||||
// This is used to check if the package has already been built.
|
// Это используется для проверки, был ли пакет уже собран.
|
||||||
func pkgFileName(vars *types.BuildVars, pkgFormat string) (string, error) {
|
func pkgFileName(vars *types.BuildVars, pkgFormat string) (string, error) {
|
||||||
pkgInfo := getBasePkgInfo(vars)
|
pkgInfo := getBasePkgInfo(vars)
|
||||||
|
|
||||||
@@ -744,9 +744,9 @@ func getSources(ctx context.Context, dirs types.Directories, bv *types.BuildVars
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !strings.EqualFold(bv.Checksums[i], "SKIP") {
|
if !strings.EqualFold(bv.Checksums[i], "SKIP") {
|
||||||
// Если контрольная сумма содержит двоеточие, используйте часть до двоеточия
|
// Если контрольная сумма содержит двоеточие, используйте часть до двоеточия
|
||||||
// как алгоритм, а часть после как фактическую контрольную сумму.
|
// как алгоритм, а часть после как фактическую контрольную сумму.
|
||||||
// В противном случае используйте sha256 по умолчанию с целой строкой как контрольной суммой.
|
// В противном случае используйте sha256 по умолчанию с целой строкой как контрольной суммой.
|
||||||
algo, hashData, ok := strings.Cut(bv.Checksums[i], ":")
|
algo, hashData, ok := strings.Cut(bv.Checksums[i], ":")
|
||||||
if ok {
|
if ok {
|
||||||
checksum, err := hex.DecodeString(hashData)
|
checksum, err := hex.DecodeString(hashData)
|
||||||
|
@@ -25,6 +25,7 @@ installPkg() {
|
|||||||
pacman) $rootCmd pacman --noconfirm -U ${@:2} ;;
|
pacman) $rootCmd pacman --noconfirm -U ${@:2} ;;
|
||||||
apk) $rootCmd apk add --allow-untrusted ${@:2} ;;
|
apk) $rootCmd apk add --allow-untrusted ${@:2} ;;
|
||||||
zypper) $rootCmd zypper --no-gpg-checks install ${@:2} ;;
|
zypper) $rootCmd zypper --no-gpg-checks install ${@:2} ;;
|
||||||
|
apt-rpm) $rootCmd apt-get install -y ${@:2} ;;
|
||||||
*) $rootCmd $1 install -y ${@:2} ;;
|
*) $rootCmd $1 install -y ${@:2} ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -59,6 +60,10 @@ elif command -v apk &>/dev/null; then
|
|||||||
info "Обнаружен apk"
|
info "Обнаружен apk"
|
||||||
pkgFormat="apk"
|
pkgFormat="apk"
|
||||||
pkgMgr="apk"
|
pkgMgr="apk"
|
||||||
|
elif command -v apt-config &>/dev/null && apt-config dump | grep -q "RPM"; then
|
||||||
|
info "Обнаружен apt-rpm"
|
||||||
|
pkgFormat="rpm"
|
||||||
|
pkgMgr="apt-rpm"
|
||||||
else
|
else
|
||||||
warn "Не обнаружен поддерживаемый менеджер пакетов!"
|
warn "Не обнаружен поддерживаемый менеджер пакетов!"
|
||||||
noPkgMgr=true
|
noPkgMgr=true
|
||||||
@@ -78,7 +83,7 @@ if [ -z "$noPkgMgr" ]; then
|
|||||||
latestFile=$(echo "$fileList" | grep -E 'alr-bin-.*.pkg.tar.zst' | sort -V | tail -n 1)
|
latestFile=$(echo "$fileList" | grep -E 'alr-bin-.*.pkg.tar.zst' | sort -V | tail -n 1)
|
||||||
elif [ "$pkgMgr" == "apt" ]; then
|
elif [ "$pkgMgr" == "apt" ]; then
|
||||||
latestFile=$(echo "$fileList" | grep -E 'alr-bin-.*.amd64.deb' | sort -V | tail -n 1)
|
latestFile=$(echo "$fileList" | grep -E 'alr-bin-.*.amd64.deb' | sort -V | tail -n 1)
|
||||||
elif [[ "$pkgMgr" == "dnf" || "$pkgMgr" == "yum" || "$pkgMgr" == "zypper" ]]; then
|
elif [ "$pkgFormat" == "rpm" ]; then
|
||||||
latestFile=$(echo "$fileList" | grep -E 'alr-bin-.*.x86_64.rpm' | sort -V | tail -n 1)
|
latestFile=$(echo "$fileList" | grep -E 'alr-bin-.*.x86_64.rpm' | sort -V | tail -n 1)
|
||||||
else
|
else
|
||||||
error "Не поддерживаемый менеджер пакетов для автоматической установки"
|
error "Не поддерживаемый менеджер пакетов для автоматической установки"
|
||||||
|
Reference in New Issue
Block a user