Compare commits

...

No commits in common. "v0.0.4" and "master" have entirely different histories.

87 changed files with 7964 additions and 2188 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
liberapay: lure

4
.gitignore vendored
View File

@ -1,3 +1,5 @@
/lure
/lure-api-server
/cmd/lure-api-server/lure-api-server
/dist/
/version.txt
/internal/config/version.txt

View File

@ -1,71 +1,95 @@
before:
hooks:
- go mod tidy
- go generate
builds:
- id: lure
env:
- CGO_ENABLED=0
binary: lure
ldflags:
- -X go.elara.ws/lure/internal/config.Version={{.Version}}
goos:
- linux
goarch:
- 386
- amd64
- arm
- 386
- arm64
- arm
- riscv64
archives:
- replacements:
386: i386
amd64: x86_64
arm64: aarch64
- name_template: >-
{{- .ProjectName}}-
{{- .Version}}-
{{- .Os}}-
{{- if .Arch | eq "amd64"}}x86_64
{{- else if .Arch | eq "386"}}i386
{{- else if .Arch | eq "arm64"}}aarch64
{{- else }}{{ .Arch }}{{ end -}}
files:
- scripts/completion/*
nfpms:
- id: lure
package_name: linux-user-repository
file_name_template: '{{.PackageName}}-{{.Version}}-{{.Os}}-{{.Arch}}'
file_name_template: >-
{{- .PackageName}}-
{{- .Version}}-
{{- .Os}}-
{{- if .Arch | eq "amd64"}}x86_64
{{- else if .Arch | eq "386"}}i386
{{- else if .Arch | eq "arm64"}}aarch64
{{- else }}{{ .Arch }}{{ end -}}
description: "Linux User REpository"
replacements:
386: i386
amd64: x86_64
arm64: aarch64
homepage: 'https://gitea.arsenm.dev/Arsen6331/lure'
maintainer: 'Arsen Musayelyan <arsen@arsenm.dev>'
homepage: 'https://lure.sh'
maintainer: 'Elara Musayelyan <elara@elara.ws>'
license: GPLv3
formats:
- apk
- deb
- rpm
- archlinux
provides:
- lure
- linux-user-repository
conflicts:
- lure
- linux-user-repository
recommends:
- aria2
contents:
- src: scripts/completion/bash
dst: /usr/share/bash-completion/completions/lure
- src: scripts/completion/zsh
dst: /usr/share/zsh/site-functions/_lure
aurs:
- name: lure-bin
homepage: 'https://gitea.arsenm.dev/Arsen6331/lure'
- name: linux-user-repository-bin
homepage: 'https://lure.sh'
description: "Linux User REpository"
maintainers:
- 'Arsen Musayelyan <arsen@arsenm.dev>'
- 'Elara Musayelyan <elara@elara.ws>'
license: GPLv3
private_key: '{{ .Env.AUR_KEY }}'
git_url: 'ssh://aur@aur.archlinux.org/lure-bin.git'
git_url: 'ssh://aur@aur.archlinux.org/linux-user-repository-bin.git'
provides:
- lure
- linux-user-repository
conflicts:
- lure
- linux-user-repository
depends:
- sudo
- pacman
optdepends:
- 'aria2: for downloading torrent sources'
package: |-
# binaries
install -Dm755 "./lure" "${pkgdir}/usr/bin/lure"
install -Dm755 ./lure "${pkgdir}/usr/bin/lure"
# completions
install -Dm755 ./scripts/completion/bash ${pkgdir}/usr/share/bash-completion/completions/lure
install -Dm755 ./scripts/completion/zsh ${pkgdir}/usr/share/zsh/site-functions/_lure
release:
gitea:
owner: Arsen6331
owner: lure
name: lure
gitea_urls:
api: 'https://gitea.arsenm.dev/api/v1/'
download: 'https://gitea.arsenm.dev'
api: 'https://gitea.elara.ws/api/v1/'
download: 'https://gitea.elara.ws'
skip_tls_verify: false
checksum:
name_template: 'checksums.txt'

View File

@ -1,3 +1,4 @@
platform: linux/amd64
pipeline:
release:
image: goreleaser/goreleaser

View File

@ -1,16 +1,20 @@
lure: version.txt
go build
PREFIX ?= /usr/local
GIT_VERSION = $(shell git describe --tags )
lure:
CGO_ENABLED=0 go build -ldflags="-X 'go.elara.ws/lure/internal/config.Version=$(GIT_VERSION)'"
clean:
rm -f lure
install: lure
sudo install -Dm755 lure /usr/local/bin/lure
install: lure installmisc
install -Dm755 lure $(DESTDIR)$(PREFIX)/bin/lure
installmisc:
install -Dm755 scripts/completion/bash $(DESTDIR)$(PREFIX)/share/bash-completion/completions/lure
install -Dm755 scripts/completion/zsh $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_lure
uninstall:
rm -f /usr/local/bin/lure
version.txt:
go generate
.PHONY: install clean uninstall
.PHONY: install clean uninstall installmisc lure

View File

@ -1,20 +1,35 @@
<img src="assets/logo.png" alt="LURE Logo" width="200">
# LURE (Linux User REpository)
[![Go Report Card](https://goreportcard.com/badge/go.arsenm.dev/lure)](https://goreportcard.com/report/go.arsenm.dev/lure)
[![status-badge](https://ci.arsenm.dev/api/badges/Arsen6331/lure/status.svg)](https://ci.arsenm.dev/Arsen6331/lure)
[![lure-bin AUR package](https://img.shields.io/aur/version/lure-bin?label=lure-bin&logo=archlinux)](https://aur.archlinux.org/packages/lure-bin/)
[![Go Report Card](https://goreportcard.com/badge/go.elara.ws/lure)](https://goreportcard.com/report/go.elara.ws/lure)
[![status-badge](https://ci.elara.ws/api/badges/lure/lure/status.svg)](https://ci.elara.ws/lure/lure)
[![SWH](https://archive.softwareheritage.org/badge/origin/https://gitea.elara.ws/lure/lure.git/)](https://archive.softwareheritage.org/browse/origin/?origin_url=https://gitea.elara.ws/lure/lure.git)
[![linux-user-repository-bin AUR package](https://img.shields.io/aur/version/linux-user-repository-bin?label=linux-user-repository-bin&logo=archlinux)](https://aur.archlinux.org/packages/linux-user-repository-bin/)
LURE is intended to bring the AUR to all distros. It is currently in an ***alpha*** state and may not be stable. It can download a repository, build packages in it using a bash script similar to [PKGBUILD](https://wiki.archlinux.org/title/PKGBUILD), and then install them using your system package manager.
LURE is a distro-agnostic build system for Linux, similar to the [AUR](https://wiki.archlinux.org/title/Arch_User_Repository). It is currently in **beta**. Most major bugs have been fixed, and most major features have been added. LURE is ready for general use, but may still break or change occasionally.
LURE is written in pure Go and has zero dependencies after it's built. The only things LURE needs are a command for privilege elevation such as `sudo`, `doas`, etc. as well as a supported package manager. Currently, LURE supports `apt`, `pacman`, `apk`, `dnf`, `yum`, and `zypper`. If a supported package manager exists on your system, it will be detected and used automatically.
LURE is written in pure Go and has zero dependencies after building. The only things LURE requires are a command for privilege elevation such as `sudo`, `doas`, etc. as well as a supported package manager. Currently, LURE supports `apt`, `pacman`, `apk`, `dnf`, `yum`, and `zypper`. If a supported package manager exists on your system, it will be detected and used automatically.
---
## Installation
Distro packages and binary archives are provided at the latest Gitea release: https://gitea.arsenm.dev/Arsen6331/lure/releases/latest
### Install script
LURE is also available on the AUR as [lure-bin](https://aur.archlinux.org/packages/lure-bin)
The LURE install script will automatically download and install the appropriate LURE package on your system. To use it, simply run the following command:
```bash
curl -fsSL lure.sh/install | bash
```
**IMPORTANT**: This will download and run the script from https://lure.sh/install. Please look through any script you download from the internet (including this one) before running it.
### Packages
Distro packages and binary archives are provided at the latest Gitea release: https://gitea.elara.ws/lure/lure/releases/latest
LURE is also available on the AUR as [linux-user-repository-bin](https://aur.archlinux.org/packages/linux-user-repository-bin)
### Building from source
@ -28,11 +43,7 @@ sudo make install
## Why?
The AUR is an amazing feature, and it's one of the main reasons I use Arch on all my daily driver devices. It is really simple while providing really useful functionality. I feel such a solution shouldn't be stuck in only a single distro, so I made LURE.
Like the AUR, it uses simple bash build scripts, but it doesn't depend on bash being installed at all. It uses an embedded, pure Go implementation of bash instead. Similarly, it uses Git to download the repos and sources, but doesn't depend on Git being installed.
This means it's really easy to deploy LURE on any distro that it has support for and on any CPU architecture. It also supports and automatically detects many package managers, so it's not limited to just `pacman`.
LURE was created because packaging software for multiple Linux distros can be difficult and error-prone, and installing those packages can be a nightmare for users unless they're available in their distro's official repositories. It automates the process of building and installing unofficial packages.
---
@ -42,25 +53,25 @@ The documentation for LURE is in the [docs](docs) directory in this repo.
---
## Web Interface
LURE has an open source web interface, licensed under the AGPLv3 (https://gitea.elara.ws/lure/lure-web), and it's available at https://lure.sh/.
---
## Repositories
Unlike the AUR, LURE supports using multiple repos. Also unlike the AUR, LURE's repos are a single git repo containing all the build scripts. Inside each LURE repo, there should be a separate directory for each package containing a `lure.sh` script, which is a PKGBUILD-like build script for LURE. The default repository is hosted on Github: https://github.com/Arsen6331/lure-repo.
LURE's repos are git repositories that contain a directory for each package, with a `lure.sh` file inside. The `lure.sh` file tells LURE how to build the package and information about it. `lure.sh` scripts are similar to the AUR's PKGBUILD scripts.
---
## Dependencies
## Acknowledgements
As mentioned before, LURE has zero dependencies after it's built. All functionality that could be pure Go is pure Go. Thanks to the following packages for making this possible:
Thanks to the following projects for making LURE possible:
- Bash: https://github.com/mvdan/sh
- Git: https://github.com/go-git/go-git
- Archiver: https://github.com/mholt/archiver
- nfpm: https://github.com/goreleaser/nfpm
---
## Planned Features
- Automated install script
- Automated docker-based testing tool
- Web interface for repos
- https://github.com/mvdan/sh
- https://github.com/go-git/go-git
- https://github.com/mholt/archiver
- https://github.com/goreleaser/nfpm
- https://github.com/charmbracelet/bubbletea
- https://gitlab.com/cznic/sqlite

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

664
build.go
View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -19,624 +19,82 @@
package main
import (
"bytes"
"context"
"encoding/hex"
"io"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/AlecAivazis/survey/v2"
_ "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/urfave/cli/v2"
"golang.org/x/exp/slices"
"github.com/goreleaser/nfpm/v2"
"github.com/goreleaser/nfpm/v2/files"
"go.arsenm.dev/lure/distro"
"go.arsenm.dev/lure/download"
"go.arsenm.dev/lure/internal/cpu"
"go.arsenm.dev/lure/internal/shutils"
"go.arsenm.dev/lure/internal/shutils/decoder"
"go.arsenm.dev/lure/manager"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
"lure.sh/lure/internal/config"
"lure.sh/lure/internal/osutils"
"lure.sh/lure/internal/types"
"lure.sh/lure/pkg/build"
"lure.sh/lure/pkg/loggerctx"
"lure.sh/lure/pkg/manager"
"lure.sh/lure/pkg/repos"
)
// BuildVars represents the script variables required
// to build a package
type BuildVars struct {
Name string `sh:"name,required"`
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"`
Replaces []string `sh:"replaces"`
Sources []string `sh:"sources"`
Checksums []string `sh:"checksums"`
Backup []string `sh:"backup"`
Scripts Scripts `sh:"scripts"`
}
type Scripts struct {
PreInstall string `sh:"preinstall"`
PostInstall string `sh:"postinstall"`
PreRemove string `sh:"preremove"`
PostRemove string `sh:"postremove"`
PreUpgrade string `sh:"preupgrade"`
PostUpgrade string `sh:"postupgrade"`
PreTrans string `sh:"pretrans"`
PostTrans string `sh:"posttrans"`
}
func buildCmd(c *cli.Context) error {
script := c.String("script")
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect supported package manager on system").Send()
}
pkgPaths, _, err := buildPackage(c.Context, script, mgr)
if err != nil {
log.Fatal("Error building package").Err(err).Send()
}
wd, err := os.Getwd()
if err != nil {
log.Fatal("Error getting working directory").Err(err).Send()
}
for _, pkgPath := range pkgPaths {
name := filepath.Base(pkgPath)
err = os.Rename(pkgPath, filepath.Join(wd, name))
if err != nil {
log.Fatal("Error moving the package").Err(err).Send()
}
}
return nil
}
func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]string, []string, error) {
info, err := distro.ParseOSRelease(ctx)
if err != nil {
return nil, nil, err
}
var distroChanged bool
if distID, ok := os.LookupEnv("LURE_DISTRO"); ok {
info.ID = distID
// Since the distro was overwritten, we don't know what the
// like distros are, so set to nil
info.Like = nil
distroChanged = true
}
fl, err := os.Open(script)
if err != nil {
return nil, nil, err
}
file, err := syntax.NewParser().Parse(fl, "lure.sh")
if err != nil {
return nil, nil, err
}
fl.Close()
env := genBuildEnv(info)
runner, err := interp.New(
interp.Env(expand.ListEnviron(env...)),
interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
interp.ExecHandler(helpers.ExecHandler),
)
if err != nil {
return nil, nil, err
}
err = runner.Run(ctx, file)
if err != nil {
return nil, nil, err
}
dec := decoder.New(info, runner)
// If distro was changed, the list of like distros
// no longer applies, so disable its use
if distroChanged {
dec.LikeDistros = false
}
fn, ok := dec.GetFunc("version")
if ok {
log.Info("Executing version()").Send()
buf := &bytes.Buffer{}
err = fn(
ctx,
interp.Dir(filepath.Dir(script)),
interp.StdIO(os.Stdin, buf, os.Stderr),
)
if err != nil {
return nil, nil, err
}
newVer := strings.TrimSpace(buf.String())
err = setVersion(ctx, runner, newVer)
if err != nil {
return nil, nil, err
}
log.Info("Updating version").Str("new", newVer).Send()
}
var vars BuildVars
err = dec.DecodeVars(&vars)
if err != nil {
return nil, nil, err
}
if !archMatches(vars.Architectures) {
var buildAnyway bool
survey.AskOne(
&survey.Confirm{
Message: "Your system's CPU architecture doesn't match this package. Do you want to build anyway?",
Default: true,
},
&buildAnyway,
)
if !buildAnyway {
os.Exit(1)
}
}
log.Info("Building package").Str("name", vars.Name).Str("version", vars.Version).Send()
baseDir := filepath.Join(cacheDir, "pkgs", vars.Name)
srcdir := filepath.Join(baseDir, "src")
pkgdir := filepath.Join(baseDir, "pkg")
err = os.RemoveAll(baseDir)
if err != nil {
return nil, nil, err
}
err = os.MkdirAll(srcdir, 0o755)
if err != nil {
return nil, nil, err
}
err = os.MkdirAll(pkgdir, 0o755)
if err != nil {
return nil, nil, err
}
installed, err := mgr.ListInstalled(nil)
if err != nil {
return nil, nil, err
}
var buildDeps []string
for _, pkgName := range vars.BuildDepends {
if _, ok := installed[pkgName]; !ok {
buildDeps = append(buildDeps, pkgName)
}
}
if len(buildDeps) > 0 {
log.Info("Installing build dependencies").Send()
installPkgs(ctx, buildDeps, mgr, false)
}
var builtDeps, builtNames, repoDeps []string
if len(vars.Depends) > 0 {
log.Info("Installing dependencies").Send()
scripts, notFound := findPkgs(vars.Depends)
for _, script := range scripts {
pkgPaths, pkgNames, err := buildPackage(ctx, script, mgr)
if err != nil {
return nil, nil, err
}
builtDeps = append(builtDeps, pkgPaths...)
builtNames = append(builtNames, pkgNames...)
builtNames = append(builtNames, filepath.Base(filepath.Dir(script)))
}
repoDeps = notFound
}
log.Info("Downloading sources").Send()
err = getSources(ctx, srcdir, &vars)
if err != nil {
return nil, nil, err
}
repodir := filepath.Dir(script)
err = setDirVars(ctx, runner, srcdir, pkgdir, repodir)
if err != nil {
return nil, nil, err
}
fn, ok = dec.GetFunc("prepare")
if ok {
log.Info("Executing prepare()").Send()
err = fn(ctx, interp.Dir(srcdir))
if err != nil {
return nil, nil, err
}
}
fn, ok = dec.GetFunc("build")
if ok {
log.Info("Executing build()").Send()
err = fn(ctx, interp.Dir(srcdir))
if err != nil {
return nil, nil, err
}
}
fn, ok = dec.GetFunc("package")
if ok {
log.Info("Executing package()").Send()
err = fn(ctx, interp.Dir(srcdir))
if err != nil {
return nil, nil, err
}
} else {
log.Fatal("The package() function is required").Send()
}
uniq(
&repoDeps,
&builtDeps,
&builtNames,
)
pkgInfo := &nfpm.Info{
Name: vars.Name,
Description: vars.Description,
Arch: runtime.GOARCH,
Version: vars.Version,
Release: strconv.Itoa(vars.Release),
Homepage: vars.Homepage,
License: strings.Join(vars.Licenses, ", "),
Maintainer: vars.Maintainer,
Overridables: nfpm.Overridables{
Conflicts: vars.Conflicts,
Replaces: vars.Replaces,
Provides: vars.Provides,
Depends: append(repoDeps, builtNames...),
var buildCmd = &cli.Command{
Name: "build",
Usage: "Build a local package",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "script",
Aliases: []string{"s"},
Value: "lure.sh",
Usage: "Path to the build script",
},
}
&cli.StringFlag{
Name: "package",
Aliases: []string{"p"},
Usage: "Name of the package to build and its repo (example: default/go-bin)",
},
&cli.BoolFlag{
Name: "clean",
Aliases: []string{"c"},
Usage: "Build package from scratch even if there's an already built package available",
},
},
Action: func(c *cli.Context) error {
ctx := c.Context
log := loggerctx.From(ctx)
if vars.Epoch != 0 {
pkgInfo.Epoch = strconv.FormatUint(uint64(vars.Epoch), 10)
}
script := c.String("script")
if c.String("package") != "" {
script = filepath.Join(config.GetPaths(ctx).RepoDir, c.String("package"), "lure.sh")
}
setScripts(&vars, pkgInfo, filepath.Dir(script))
err := repos.Pull(ctx, config.Config(ctx).Repos)
if err != nil {
log.Fatal("Error pulling repositories").Err(err).Send()
}
if slices.Contains(vars.Architectures, "all") {
pkgInfo.Arch = "all"
}
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect a supported package manager on the system").Send()
}
if pkgInfo.Arch == "arm" {
pkgInfo.Arch = cpu.ARMVariant()
}
pkgPaths, _, err := build.BuildPackage(ctx, types.BuildOpts{
Script: script,
Manager: mgr,
Clean: c.Bool("clean"),
Interactive: c.Bool("interactive"),
})
if err != nil {
log.Fatal("Error building package").Err(err).Send()
}
contents := []*files.Content{}
filepath.Walk(pkgdir, func(path string, fi os.FileInfo, err error) error {
trimmed := strings.TrimPrefix(path, pkgdir)
wd, err := os.Getwd()
if err != nil {
log.Fatal("Error getting working directory").Err(err).Send()
}
if fi.IsDir() {
f, err := os.Open(path)
for _, pkgPath := range pkgPaths {
name := filepath.Base(pkgPath)
err = osutils.Move(pkgPath, filepath.Join(wd, name))
if err != nil {
return err
log.Fatal("Error moving the package").Err(err).Send()
}
_, 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(),
},
})
f.Close()
return nil
}
if fi.Mode()&os.ModeSymlink != 0 {
link, err := os.Readlink(path)
if err != nil {
return err
}
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
})
pkgInfo.Overridables.Contents = contents
pkgFormat := mgr.Format()
if format, ok := os.LookupEnv("LURE_PKG_FORMAT"); ok {
pkgFormat = format
}
packager, err := nfpm.Get(pkgFormat)
if err != nil {
return nil, nil, err
}
pkgName := packager.ConventionalFileName(pkgInfo)
pkgPath := filepath.Join(baseDir, pkgName)
pkgPaths := append(builtDeps, pkgPath)
pkgNames := append(builtNames, vars.Name)
pkgFile, err := os.Create(pkgPath)
if err != nil {
return nil, nil, err
}
err = packager.Package(pkgInfo, pkgFile)
if err != nil {
return nil, nil, err
}
if len(buildDeps) > 0 {
var removeBuildDeps bool
err = survey.AskOne(&survey.Confirm{
Message: "Would you like to remove build dependencies?",
}, &removeBuildDeps)
if err != nil {
return nil, nil, err
}
if removeBuildDeps {
err = mgr.Remove(
&manager.Opts{
AsRoot: true,
NoConfirm: true,
},
buildDeps...,
)
if err != nil {
return nil, nil, err
}
}
}
uniq(&pkgPaths, &pkgNames)
return pkgPaths, pkgNames, nil
}
func genBuildEnv(info *distro.OSRelease) []string {
env := os.Environ()
arch := runtime.GOARCH
if arch == "arm" {
arch = cpu.ARMVariant()
}
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="+arch,
"NCPU="+strconv.Itoa(runtime.NumCPU()),
)
return env
}
func getSources(ctx context.Context, srcdir string, bv *BuildVars) error {
if len(bv.Sources) != len(bv.Checksums) {
log.Fatal("The checksums array must be the same length as sources")
}
for i, src := range bv.Sources {
opts := download.GetOptions{
SourceURL: src,
Destination: srcdir,
EncloseGit: true,
}
if !strings.EqualFold(bv.Checksums[i], "SKIP") {
checksum, err := hex.DecodeString(bv.Checksums[i])
if err != nil {
return err
}
opts.SHA256Sum = checksum
}
err := download.Get(ctx, opts)
if err != nil {
return err
}
}
return nil
}
// setDirVars sets srcdir and pkgdir. It's a very hacky way of doing so,
// but setting the runner's Env and Vars fields doesn't seem to work.
func setDirVars(ctx context.Context, runner *interp.Runner, srcdir, pkgdir, repodir string) error {
cmd := "srcdir='" + srcdir + "'\npkgdir='" + pkgdir + "'\nrepodir='" + repodir + "'\n"
fl, err := syntax.NewParser().Parse(strings.NewReader(cmd), "vars")
if err != nil {
return err
}
return runner.Run(ctx, fl)
}
func setScripts(vars *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)
}
}
// getBuildVars only gets the build variables, while disabling exec, stat, open, and readdir
func getBuildVars(ctx context.Context, script string, info *distro.OSRelease) (*BuildVars, error) {
fl, err := os.Open(script)
if err != nil {
return nil, err
}
file, err := syntax.NewParser().Parse(fl, "lure.sh")
if err != nil {
return nil, err
}
fl.Close()
runner, err := interp.New(
interp.Env(expand.ListEnviron()),
interp.ExecHandler(shutils.NopExec),
interp.StatHandler(shutils.NopStat),
interp.OpenHandler(shutils.NopOpen),
interp.ReadDirHandler(shutils.NopReadDir),
)
if err != nil {
return nil, err
}
err = runner.Run(ctx, file)
if err != nil {
return nil, err
}
dec := decoder.New(info, runner)
var vars BuildVars
err = dec.DecodeVars(&vars)
if err != nil {
return nil, err
}
return &vars, nil
}
func archMatches(architectures []string) bool {
if slices.Contains(architectures, "all") {
return true
}
arch := runtime.GOARCH
if arch == "arm" {
arch = cpu.ARMVariant()
}
if slices.Contains(architectures, "arm") {
architectures = append(architectures, cpu.ARMVariant())
}
return slices.Contains(architectures, arch)
}
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)
}
func uniq(ss ...*[]string) {
for _, s := range ss {
slices.Sort(*s)
*s = slices.Compact(*s)
}
},
}

125
config.go
View File

@ -1,125 +0,0 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"os"
"path/filepath"
"github.com/pelletier/go-toml/v2"
"go.arsenm.dev/lure/manager"
)
var (
cacheDir string
cfgPath string
config Config
)
type Config struct {
RootCmd string `toml:"rootCmd"`
Repos []Repo `toml:"repo"`
}
type Repo struct {
Name string `toml:"name"`
URL string `toml:"url"`
}
var defaultConfig = Config{
RootCmd: "sudo",
Repos: []Repo{
{
Name: "default",
URL: "https://github.com/Arsen6331/lure-repo.git",
},
},
}
func init() {
cfg, cache, err := makeDirs()
if err != nil {
log.Fatal("Error creating directories").Err(err).Send()
}
cacheDir = cache
cfgPath = filepath.Join(cfg, "lure.toml")
cfgFl, err := os.Open(cfgPath)
if err != nil {
log.Fatal("Error opening config file").Err(err).Send()
}
defer cfgFl.Close()
err = toml.NewDecoder(cfgFl).Decode(&config)
if err != nil {
log.Fatal("Error decoding config file").Err(err).Send()
}
manager.DefaultRootCmd = config.RootCmd
}
func makeDirs() (string, string, error) {
cfgDir, err := os.UserConfigDir()
if err != nil {
return "", "", err
}
baseCfgPath := filepath.Join(cfgDir, "lure")
err = os.MkdirAll(baseCfgPath, 0o755)
if err != nil {
return "", "", err
}
cfgPath := filepath.Join(baseCfgPath, "lure.toml")
if _, err := os.Stat(cfgPath); err != nil {
cfgFl, err := os.Create(cfgPath)
if err != nil {
return "", "", err
}
err = toml.NewEncoder(cfgFl).Encode(&defaultConfig)
if err != nil {
return "", "", err
}
cfgFl.Close()
}
cacheDir, err := os.UserCacheDir()
if err != nil {
return "", "", err
}
baseCachePath := filepath.Join(cacheDir, "lure")
err = os.MkdirAll(filepath.Join(baseCachePath, "repo"), 0o755)
if err != nil {
return "", "", err
}
err = os.MkdirAll(filepath.Join(baseCachePath, "pkgs"), 0o755)
if err != nil {
return "", "", err
}
return baseCfgPath, baseCachePath, nil
}

View File

@ -1,6 +1,8 @@
# LURE Docs
- [Build Scripts](build-scripts.md)
- [Usage](usage.md)
- [Configuration](configuration.md)
- [Adding Packages to LURE's repo](adding-packages.md)
- [Usage](usage.md)
- [Packages](packages)
- [Build Scripts](packages/build-scripts.md)
- [Package Conventions](packages/conventions.md)
- [Adding Packages to LURE's repo](packages/adding-packages.md)

View File

@ -36,7 +36,7 @@ The `repo` array in the config specifies which repos are added to LURE. Each rep
```toml
[[repo]]
name = 'default'
url = 'https://github.com/Arsen6331/lure-repo.git'
url = 'https://github.com/Elara6331/lure-repo.git'
```
The `default` repo is added by default. Any amount of repos may be added.

5
docs/packages/README.md Normal file
View File

@ -0,0 +1,5 @@
# LURE Docs > Packages
- [Build Scripts](build-scripts.md)
- [Package Conventions](conventions.md)
- [Adding Packages to LURE's repo](adding-packages.md)

View File

@ -5,19 +5,23 @@
- `go` (1.18+)
- `git`
- `lure-analyzer`
- `go install go.arsenm.dev/lure-repo-bot/cmd/lure-analyzer@latest`
- `go install go.elara.ws/lure-repo-bot/cmd/lure-analyzer@latest`
- `shfmt`
- May be available in distro repos
- `go install mvdan.cc/sh/v3/cmd/shfmt@latest`
---
## How to test a package
To test packages you can first create [a `lure.sh` shell file](./build-scripts.md) and then run the `lure build` comand to build the local `lure.sh` file into a package for your distro (more info about the `build` command [here](./usage.md#build)). You can then install this file to your distro and test it.
## How to submit a package
LURE's repo is hosted on Github at https://github.com/Arsen6331/lure-repo. In it, there are multiple directories each containing a `lure.sh` file. In order to add a package to LURE's repo, simply create a PR with a [build script](./build-scripts.md) and place it in a directory with the same name as the package.
LURE's repo is hosted on Github at https://github.com/Elara6331/lure-repo. In it, there are multiple directories each containing a `lure.sh` file. In order to add a package to LURE's repo, simply create a PR with a [build script](./build-scripts.md) and place it in a directory with the same name as the package.
Upon submitting the PR, [lure-repo-bot](https://github.com/Arsen6331/lure-repo-bot) will pull your PR and analyze it, providing suggestions for fixes as review comments. If there are no problems, the bot will approve your changes. If there are issues, re-request review from the bot after you've finished applying the fixes and it will automatically review the PR again.
Upon submitting the PR, [lure-repo-bot](https://github.com/Elara6331/lure-repo-bot) will pull your PR and analyze it, providing suggestions for fixes as review comments. If there are no problems, the bot will approve your changes. If there are issues, re-request review from the bot after you've finished applying the fixes and it will automatically review the PR again.
All scripts submitted to the LURE repo should be formatted with `shfmt`. If they are not properly formatted, Github Actions will add suggestions in the "Files Changed" tab of the PR.
Once your PR is merged, LURE will pull the changed repo and your package will be available for people to install.
Once your PR is merged, LURE will pull the changed repo and your package will be available for people to install.

View File

@ -21,6 +21,7 @@ LURE uses build scripts similar to the AUR's PKGBUILDs. This is the documentatio
- [conflicts](#conflicts)
- [deps](#deps)
- [build_deps](#build_deps)
- [opt_deps](#opt_deps)
- [replaces](#replaces)
- [sources](#sources)
- [checksums](#checksums)
@ -48,12 +49,13 @@ LURE uses build scripts similar to the AUR's PKGBUILDs. This is the documentatio
- [install-manual](#install-manual)
- [install-desktop](#install-desktop)
- [install-library](#install-library)
- [git-version](#git-version)
---
## Distro Overrides
Allowing LURE to run on different distros provides some challenges. For example, some distros use different names for their packages. This is solved using distro overrides. Any variable or function used in a LURE build script may be overridden based on distro and CPU architecture. The way you do this is by appending the distro and/or architecture to the end of the name. For example, [ITD](https://gitea.arsenm.dev/Arsen6331/itd) depends on the `pactl` command as well as DBus and BlueZ. These are named somewhat differently on different distros. For ITD, I use the following for the dependencies:
Allowing LURE to run on different distros provides some challenges. For example, some distros use different names for their packages. This is solved using distro overrides. Any variable or function used in a LURE build script may be overridden based on distro and CPU architecture. The way you do this is by appending the distro and/or architecture to the end of the name. For example, [ITD](https://gitea.elara.ws/Elara6331/itd) depends on the `pactl` command as well as DBus and BlueZ. These are named somewhat differently on different distros. For ITD, I use the following for the dependencies:
```bash
deps=('dbus' 'bluez' 'pulseaudio-utils')
@ -116,7 +118,7 @@ The `homepage` field contains the URL to the website of the project packaged by
The `maintainer` field contains the name and email address of the person maintaining the package. Example:
```text
Arsen Musayelyan <arsen@arsenm.dev>
Elara Musayelyan <elara@elara.ws>
```
While LURE does not require this field to be set, Debian has deprecated unset maintainer fields, and may disallow their use in `.deb` packages in the future.
@ -159,6 +161,17 @@ The `deps` array contains the dependencies for the package. LURE repos will be c
The `build_deps` array contains the dependencies that are required to build the package. They will be installed before the build starts. Similarly to the `deps` array, LURE repos will be checked first.
### opt_deps
The `opt_deps` array contains optional dependencies for the package. A description can be added after ": ", but it's not required.
```bash
opt_deps_arch=(
'git: Download git repositories'
'aria2: Download files'
)
```
### replaces
The `replaces` array contains the packages that are replaced by this package. Generally, if package managers find a package with a `replaces` field set, they will remove the listed package(s) and install that one instead. This is only useful if the packages are being stored in a repo for your package manager.
@ -181,25 +194,26 @@ https://example.com/archive.tar.gz?~archive=false
If the URL scheme starts with `git+`, the source will be downloaded as a git repo. The git download mode supports multiple parameters:
- `~tag`: Specify which tag of the repo to check out.
- `~branch`: Specify which branch of the repo to check out.
- `~commit`: Specify which commit of the repo to check out.
- `~rev`: Specify which revision of the repo to check out.
- `~depth`: Specify what depth should be used when cloning the repo. Must be an integer.
- `~name`: Specify the name of the directory into which the git repo should be cloned.
- `~recursive`: If set to true, submodules will be cloned recursively. It is false by default.
Examples:
```text
git+https://gitea.arsenm.dev/Arsen6331/itd?~branch=resource-loading&~depth=1
git+https://gitea.elara.ws/Elara6331/itd?~rev=resource-loading&~depth=1
```
```text
git+https://gitea.arsenm.dev/Arsen6331/lure?~tag=v0.0.1
git+https://gitea.elara.ws/lure/lure?~rev=v0.0.1&~recursive=true
```
### checksums
The `checksums` array must be the same length as the `sources` array. It contains sha256 checksums for the source files. The files are checked against the checksums and the build fails if they don't match.
The `checksums` array must be the same length as the `sources` array. It contains checksums for the source files. The files are checked against the checksums and the build fails if they don't match.
By default, checksums are expected to be sha256. To change the algorithm, add it before the hash with a colon in between. For example, `md5:bc0c6f5dcd06bddbca9a0163e4c9f2e1`. The following algorithms are currently supported: `sha256`, `sha224`, `sha512`, `sha384`, `sha1`, and `md5`.
To skip the check for a particular source, set the corresponding checksum to `SKIP`.
@ -242,11 +256,11 @@ The rest of the scripts are available in all packages.
This section documents user-defined functions that can be added to build scripts. Any functions marked with `(*)` are required.
All functions except for `version()` are executed in the `$srcdir` directory
All functions are executed in the `$srcdir` directory
### version
The `version()` function is the first to run. It updates the `version` variable. This allows for automatically deriving the version from sources. This is most useful for git packages, which usually don't need to be changed, so their `version` variable stays the same.
The `version()` function updates the `version` variable. This allows for automatically deriving the version from sources. This is most useful for git packages, which usually don't need to be changed, so their `version` variable stays the same.
An example of using this for git:
@ -259,8 +273,6 @@ version() {
The AUR equivalent is the [`pkgver()` function](https://wiki.archlinux.org/title/VCS_package_guidelines#The_pkgver()_function)
This function does not run in `$srcdir` because it is executed before the source directory is even created. Instead, it runs in `$repodir`.
### prepare
The `prepare()` function is meant to prepare the sources for building and packaging. This is the function in which patches should be applied, for example, by the `patch` command, and where tools like `go generate` should be executed.
@ -424,7 +436,7 @@ Examples:
```bash
./k9s completion fish | install-completion fish k9s
install-completion bash k9s <./k9s/completions/k9s.fish
install-completion bash k9s <./k9s/completions/k9s.bash
```
### install-manual
@ -467,4 +479,21 @@ Examples:
```bash
install-library ./${name}/build/libadldap.so
```
### git-version
`git-version` returns a version number based on the git revision of a repository.
If an argument is provided, it will be used as the path to the repo. Otherwise, the current directory will be used.
The version number will be the amount of revisions, a dot, and the short hash of the current revision. For example: `118.e4b8348`.
The AUR's convention includes an `r` at the beginning of the version number. This is ommitted because some distros expect the version number to start with a digit.
Examples:
```bash
git-version
git-version "$srcdir/itd"
```

View File

@ -0,0 +1,36 @@
# Package Conventions
## General
Packages should have the name(s) of what they contain in their `provides` and `conflicts` arrays. That way, they can be installed by users without needing to know the full package name. For example, there are two LURE packages for ITD: `itd-bin`, and `itd-git`. Both of them have provides and conflicts arrays specifying the two commands they install: `itd`, and `itctl`. This means that if a user wants to install ITD, they simply have to type `lure in itd` and LURE will prompt them for which one they want to install.
## Binary packages
Packages that install download and install precompiled binaries should have a `-bin` suffix.
## Git packages
Packages that build and install programs from source code cloned directly from Git should have a `-git` suffix.
The versions of these packages should consist of the amount of revisions followed by the current revision, separated by a period. For example: `183.80187b0`. Note that unlike the AUR, there is no `r` at the beginning. This is because some package managers refuse to install packages whose version numbers don't start with a digit.
This version number can be obtained using the following command:
```bash
printf "%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
```
The `version()` function for such packages should use the LURE-provided `git-version` helper command, like so:
```bash
version() {
cd "$srcdir/$name"
git-version
}
```
This uses LURE's embedded Git implementation, which ensures that the user doesn't need Git installed on their system in order to install `-git` packages.
## Other packages
Packages that download sources for a specific version of a program should not have any suffix, even if those sources are downloaded from Git.

View File

@ -12,6 +12,8 @@
- [addrepo](#addrepo)
- [removerepo](#removerepo)
- [refresh](#refresh)
- [fix](#fix)
- [version](#version)
- [Environment Variables](#environment-variables)
- [LURE_DISTRO](#lure_distro)
- [LURE_PKG_FORMAT](#lure_pkg_format)
@ -23,12 +25,21 @@
### install
The install command installs a command from the LURE repos. Any packages that aren't found in LURE's repos get forwarded to the system package manager for installation.
The install command installs a package from the LURE repos. Any packages that aren't found in LURE's repos get forwarded to the system package manager for installation.
Example:
The package arguments do not have to be exact. LURE will check the `provides` array if an exact match is not found. There is also support for using "%" as a wildcard.
If multiple packages are found, you will be prompted to select which you want to install.
By default, if a package has already been built, LURE will install the cached package rather than re-build it. Use the `-c` or `--clean` flag to force a re-build.
Examples:
```shell
lure in itd-bin
lure in itd-bin # only finds itd-bin
lure in itd # finds itd-bin and itd-git
lure in it% # finds itd-bin, itd-git, and itgui-git
lure in -c itd-bin
```
### remove
@ -45,6 +56,8 @@ lure rm firefox
The upgrade command looks through the packages installed on your system and sees if any of them match LURE repo packages. If they do, their versions are compared using the `rpmvercmp` algorithm. If LURE repos contain a newer version, the package is upgraded.
By default, if a package has already been built, LURE will install the cached package rather than re-build it. Use the `-c` or `--clean` flag to force a re-build.
Example:
```shell
@ -55,20 +68,36 @@ lure up
The info command displays information about a package in LURE's repos.
The package arguments do not have to be exact. LURE will check the `provides` array if an exact match is not found. There is also support for using "%" as a wildcard.
If multiple packages are found, you will be prompted to select which you want to show.
Example:
```shell
lure info itd-bin
lure info itd-bin # only finds itd-bin
lure info itd # finds itd-bin and itd-git
lure info it% # finds itd-bin, itd-git, and itgui-git
```
### list
The list command lists all LURE repo packages as well as their versions
Example:
This command accepts a single optional argument. This argument is a pattern to filter found packages against.
The pattern does not have to be exact. LURE will check the `provides` array if an exact match is not found. There is also support for using "%" as a wildcard.
There is a `-I` or `--installed` flag that filters out any packages that are not installed on the system
Examples:
```shell
lure ls
lure ls # lists all LURE packages
lure ls -I # lists all installed packages
lure ls i% # lists all packages starting with "i"
lure ls %d # lists all packages ending with "d"
lure ls -I i% # lists all installed packages that start with "i"
```
### build
@ -88,7 +117,7 @@ The addrepo command adds a repository to LURE if it doesn't already exist. The `
Example:
```shell
lure ar -n default -u https://github.com/Arsen6331/lure-repo
lure ar -n default -u https://github.com/Elara6331/lure-repo
```
### removerepo
@ -111,6 +140,26 @@ Example:
lure ref
```
### fix
The fix command attempts to fix issues with LURE by deleting and rebuilding LURE's cache
Example:
```shell
lure fix
```
### version
The version command returns the current LURE version and exits
Example:
```shell
lure version
```
---
## Environment Variables

View File

@ -1,292 +0,0 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package download
import (
"bytes"
"context"
"crypto/sha256"
"errors"
"hash"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/mholt/archiver/v4"
)
var ErrChecksumMismatch = errors.New("checksums did not match")
type GetOptions struct {
SourceURL string
Destination string
SHA256Sum []byte
// EncloseGit determines if Get will create an enclosing
// directory for git repos
EncloseGit bool
}
// Get downloads from a URL
func Get(ctx context.Context, opts GetOptions) error {
dest, err := filepath.Abs(opts.Destination)
if err != nil {
return err
}
opts.Destination = dest
err = os.MkdirAll(opts.Destination, 0o755)
if err != nil {
return err
}
src, err := url.Parse(opts.SourceURL)
if err != nil {
return err
}
query := src.Query()
if strings.HasPrefix(src.Scheme, "git+") {
err = getGit(ctx, src, query, opts)
if err != nil {
return err
}
} else {
err = getFile(ctx, src, query, opts)
if err != nil {
return err
}
}
return nil
}
func getGit(ctx context.Context, src *url.URL, query url.Values, opts GetOptions) (err error) {
tag := query.Get("~tag")
query.Del("~tag")
branch := query.Get("~branch")
query.Del("~branch")
commit := query.Get("~commit")
query.Del("~commit")
depthStr := query.Get("~depth")
query.Del("~depth")
name := query.Get("~name")
query.Del("~name")
var refName plumbing.ReferenceName
if tag != "" {
refName = plumbing.NewTagReferenceName(tag)
} else if branch != "" {
refName = plumbing.NewBranchReferenceName(branch)
}
src.Scheme = strings.TrimPrefix(src.Scheme, "git+")
src.RawQuery = query.Encode()
if name == "" {
name = path.Base(src.Path)
name = strings.TrimSuffix(name, ".git")
}
dstDir := opts.Destination
if opts.EncloseGit {
dstDir = filepath.Join(opts.Destination, name)
}
depth := 0
if depthStr != "" {
depth, err = strconv.Atoi(depthStr)
if err != nil {
return err
}
}
cloneOpts := &git.CloneOptions{
URL: src.String(),
Progress: os.Stderr,
Depth: depth,
}
repo, err := git.PlainCloneContext(ctx, dstDir, false, cloneOpts)
if err != nil {
return err
}
w, err := repo.Worktree()
if err != nil {
return err
}
checkoutOpts := &git.CheckoutOptions{}
if refName != "" {
checkoutOpts.Branch = refName
} else if commit != "" {
checkoutOpts.Hash = plumbing.NewHash(commit)
} else {
return nil
}
return w.Checkout(checkoutOpts)
}
func getFile(ctx context.Context, src *url.URL, query url.Values, opts GetOptions) error {
name := query.Get("~name")
query.Del("~name")
archive := query.Get("~archive")
query.Del("~archive")
src.RawQuery = query.Encode()
if name == "" {
name = path.Base(src.Path)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, src.String(), nil)
if err != nil {
return err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
hash := sha256.New()
format, input, err := archiver.Identify(name, res.Body)
if err == archiver.ErrNoMatch || archive == "false" {
fl, err := os.Create(filepath.Join(opts.Destination, name))
if err != nil {
return err
}
w := io.MultiWriter(hash, fl)
_, err = io.Copy(w, input)
if err != nil {
return err
}
res.Body.Close()
fl.Close()
if opts.SHA256Sum != nil {
sum := hash.Sum(nil)
if !bytes.Equal(opts.SHA256Sum, sum) {
return ErrChecksumMismatch
}
}
} else if err != nil {
return err
} else {
err = extractFile(ctx, input, hash, format, name, opts)
if err != nil {
return err
}
}
return nil
}
func extractFile(ctx context.Context, input io.Reader, hash hash.Hash, format archiver.Format, name string, opts GetOptions) (err error) {
r := io.TeeReader(input, hash)
fname := format.Name()
switch format := format.(type) {
case archiver.Extractor:
err = format.Extract(ctx, r, nil, func(ctx context.Context, f archiver.File) error {
fr, err := f.Open()
if err != nil {
return err
}
defer fr.Close()
fi, err := f.Stat()
if err != nil {
return err
}
fm := fi.Mode()
path := filepath.Join(opts.Destination, f.NameInArchive)
err = os.MkdirAll(filepath.Dir(path), 0o755)
if err != nil {
return err
}
if f.IsDir() {
err = os.Mkdir(path, 0o755)
if err != nil {
return err
}
} else {
outFl, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fm.Perm())
if err != nil {
return err
}
defer outFl.Close()
_, err = io.Copy(outFl, fr)
return err
}
return nil
})
if err != nil {
return err
}
case archiver.Decompressor:
rc, err := format.OpenReader(r)
if err != nil {
return err
}
defer rc.Close()
path := filepath.Join(opts.Destination, name)
path = strings.TrimSuffix(path, fname)
outFl, err := os.Create(path)
if err != nil {
return err
}
_, err = io.Copy(outFl, rc)
if err != nil {
return err
}
}
if opts.SHA256Sum != nil {
sum := hash.Sum(nil)
if !bytes.Equal(opts.SHA256Sum, sum) {
return ErrChecksumMismatch
}
}
return nil
}

64
fix.go Normal file
View File

@ -0,0 +1,64 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"os"
"github.com/urfave/cli/v2"
"lure.sh/lure/internal/config"
"lure.sh/lure/internal/db"
"lure.sh/lure/pkg/loggerctx"
"lure.sh/lure/pkg/repos"
)
var fixCmd = &cli.Command{
Name: "fix",
Usage: "Attempt to fix problems with LURE",
Action: func(c *cli.Context) error {
ctx := c.Context
log := loggerctx.From(ctx)
db.Close()
paths := config.GetPaths(ctx)
log.Info("Removing cache directory").Send()
err := os.RemoveAll(paths.CacheDir)
if err != nil {
log.Fatal("Unable to remove cache directory").Err(err).Send()
}
log.Info("Rebuilding cache").Send()
err = os.MkdirAll(paths.CacheDir, 0o755)
if err != nil {
log.Fatal("Unable to create new cache directory").Err(err).Send()
}
err = repos.Pull(ctx, config.Config(ctx).Repos)
if err != nil {
log.Fatal("Error pulling repos").Err(err).Send()
}
log.Info("Done").Send()
return nil
},
}

45
gen.go Normal file
View File

@ -0,0 +1,45 @@
package main
import (
"os"
"github.com/urfave/cli/v2"
"lure.sh/lure/pkg/gen"
)
var genCmd = &cli.Command{
Name: "generate",
Usage: "Generate a LURE script from a template",
Aliases: []string{"gen"},
Subcommands: []*cli.Command{
genPipCmd,
},
}
var genPipCmd = &cli.Command{
Name: "pip",
Usage: "Generate a LURE script for a pip module",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Required: true,
},
&cli.StringFlag{
Name: "version",
Aliases: []string{"v"},
Required: true,
},
&cli.StringFlag{
Name: "description",
Aliases: []string{"d"},
},
},
Action: func(c *cli.Context) error {
return gen.Pip(os.Stdout, gen.PipOptions{
Name: c.String("name"),
Version: c.String("version"),
Description: c.String("description"),
})
},
}

130
go.mod
View File

@ -1,74 +1,126 @@
module go.arsenm.dev/lure
module lure.sh/lure
go 1.18
go 1.21
toolchain go1.21.3
require (
github.com/AlecAivazis/survey/v2 v2.3.6
github.com/go-git/go-git/v5 v5.4.2
github.com/goreleaser/nfpm/v2 v2.20.0
github.com/mholt/archiver/v4 v4.0.0-alpha.7
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/PuerkitoBio/purell v1.2.0
github.com/alecthomas/chroma/v2 v2.9.1
github.com/charmbracelet/bubbles v0.16.1
github.com/charmbracelet/bubbletea v0.24.2
github.com/charmbracelet/lipgloss v0.8.0
github.com/go-git/go-billy/v5 v5.5.0
github.com/go-git/go-git/v5 v5.9.0
github.com/goreleaser/nfpm/v2 v2.33.0
github.com/jmoiron/sqlx v1.3.5
github.com/mattn/go-isatty v0.0.19
github.com/mholt/archiver/v4 v4.0.0-alpha.8
github.com/mitchellh/mapstructure v1.5.0
github.com/pelletier/go-toml/v2 v2.0.5
github.com/urfave/cli/v2 v2.16.3
go.arsenm.dev/logger v0.0.0-20220630204155-5ba23e583f0a
golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f
github.com/muesli/reflow v0.3.0
github.com/pelletier/go-toml/v2 v2.1.0
github.com/schollz/progressbar/v3 v3.13.1
github.com/urfave/cli/v2 v2.25.7
github.com/vmihailenco/msgpack/v5 v5.3.5
go.elara.ws/logger v0.0.0-20230421022458-e80700db2090
go.elara.ws/translate v0.0.0-20230421025926-32ccfcd110e6
go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4
golang.org/x/crypto v0.13.0
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
golang.org/x/sys v0.12.0
golang.org/x/text v0.13.0
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.5.1
lure.sh/fakeroot v0.0.0-20231024000108-b130d64a68ee
modernc.org/sqlite v1.25.0
mvdan.cc/sh/v3 v3.7.0
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20210512092938-c05353c2d58c // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
github.com/bodgit/plumbing v1.2.0 // indirect
github.com/bodgit/sevenzip v1.3.0 // indirect
github.com/bodgit/windows v1.0.0 // indirect
github.com/cavaliergopher/cpio v1.0.1 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/connesc/cipherio v0.2.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/frankban/quicktest v1.14.3 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/rpmpack v0.5.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gookit/color v1.5.1 // indirect
github.com/goreleaser/chglog v0.2.2 // indirect
github.com/goreleaser/chglog v0.5.0 // indirect
github.com/goreleaser/fileglob v1.3.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.1.0 // indirect
github.com/klauspost/compress v1.15.11 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/skeema/knownhosts v1.2.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/xanzy/ssh-agent v0.3.1 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/term v0.12.0 // indirect
golang.org/x/tools v0.13.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.24.1 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.6.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
)

607
go.sum
View File

@ -1,237 +1,565 @@
github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw=
github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=
github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/ProtonMail/go-crypto v0.0.0-20210512092938-c05353c2d58c h1:bNpaLLv2Y4kslsdkdCwAYu8Bak1aGVtxwi8Z/wy4Yuo=
github.com/ProtonMail/go-crypto v0.0.0-20210512092938-c05353c2d58c/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f h1:CGq7OieOz3wyQJ1fO8S0eO9TCW1JyvLrf8fhzz1i8ko=
github.com/ProtonMail/gopenpgp/v2 v2.2.2 h1:u2m7xt+CZWj88qK1UUNBoXeJCFJwJCZ/Ff4ymGoxEXs=
github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/gopenpgp/v2 v2.7.1 h1:Awsg7MPc2gD3I7IFac2qE3Gdls0lZW8SzrFZ3k1oz0s=
github.com/ProtonMail/gopenpgp/v2 v2.7.1/go.mod h1:/BU5gfAVwqyd8EfC3Eu7zmuhwYQpKs+cGD8M//iiaxs=
github.com/PuerkitoBio/purell v1.2.0 h1:/Jdm5QfyM8zdlqT6WVZU4cfP23sot6CEHA4CS49Ezig=
github.com/PuerkitoBio/purell v1.2.0/go.mod h1:OhLRTaaIzhvIyofkJfB24gokC7tM42Px5UhoT32THBk=
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
github.com/alecthomas/chroma/v2 v2.9.1 h1:0O3lTQh9FxazJ4BYE/MOi/vDGuHn7B+6Bu902N2UZvU=
github.com/alecthomas/chroma/v2 v2.9.1/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
github.com/bodgit/plumbing v1.2.0 h1:gg4haxoKphLjml+tgnecR4yLBV5zo4HAZGCtAh3xCzM=
github.com/bodgit/plumbing v1.2.0/go.mod h1:b9TeRi7Hvc6Y05rjm8VML3+47n4XTZPtQ/5ghqic2n8=
github.com/bodgit/sevenzip v1.3.0 h1:1ljgELgtHqvgIp8W8kgeEGHIWP4ch3xGI8uOBZgLVKY=
github.com/bodgit/sevenzip v1.3.0/go.mod h1:omwNcgZTEooWM8gA/IJ2Nk/+ZQ94+GsytRzOJJ8FBlM=
github.com/bodgit/windows v1.0.0 h1:rLQ/XjsleZvx4fR1tB/UxQrK+SJ2OFHzfPjLWWOhDIA=
github.com/bodgit/windows v1.0.0/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/caarlos0/go-rpmutils v0.2.1-0.20211112020245-2cd62ff89b11 h1:IRrDwVlWQr6kS1U8/EtyA1+EHcc4yl8pndcqXWrEamg=
github.com/caarlos0/go-rpmutils v0.2.1-0.20211112020245-2cd62ff89b11/go.mod h1:je2KZ+LxaCNvCoKg32jtOIULcFogJKcL1ZWUaIBjKj0=
github.com/caarlos0/testfs v0.4.4 h1:3PHvzHi5Lt+g332CiShwS8ogTgS3HjrmzZxCm6JCDr8=
github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk=
github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY=
github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc=
github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY=
github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
github.com/charmbracelet/lipgloss v0.8.0 h1:IS00fk4XAHcf8uZKc3eHeMUTCxUH6NkaTrdyCQk84RU=
github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja6y+7DniDDw5KKU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw=
github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34=
github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8=
github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4=
github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
github.com/go-git/go-git/v5 v5.9.0 h1:cD9SFA7sHVRdJ7AYck1ZaAa/yeuBvGPxwXDL8cxrObY=
github.com/go-git/go-git/v5 v5.9.0/go.mod h1:RKIqga24sWdMGZF+1Ekv9kylsDz6LzdTSI2s/OsZWE0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/rpmpack v0.5.0 h1:L16KZ3QvkFGpYhmp23iQip+mx1X39foEsqszjMNBm8A=
github.com/google/rpmpack v0.5.0/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gookit/color v1.5.1 h1:Vjg2VEcdHpwq+oY63s/ksHrgJYCTo0bwWvmmYWdE9fQ=
github.com/gookit/color v1.5.1/go.mod h1:wZFzea4X8qN6vHOSP2apMb4/+w/orMznEzYsIHPaqKM=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/goreleaser/chglog v0.2.2 h1:V7nf07baXtGAgGevvqgW2MM4kZ6gOr12vKNSAU3VIZ0=
github.com/goreleaser/chglog v0.2.2/go.mod h1:2s5JwtCOWjZa8AIneL+xdUl9SRuigCjRHNHsX30dupE=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/goreleaser/chglog v0.5.0 h1:Sk6BMIpx8+vpAf8KyPit34OgWui8c7nKTMHhYx88jJ4=
github.com/goreleaser/chglog v0.5.0/go.mod h1:Ri46M3lrMuv76FHszs3vtABR8J8k1w9JHYAzxeeOl28=
github.com/goreleaser/fileglob v1.3.0 h1:/X6J7U8lbDpQtBvGcwwPS6OpzkNVlVEsFUVRx9+k+7I=
github.com/goreleaser/fileglob v1.3.0/go.mod h1:Jx6BoXv3mbYkEzwm9THo7xbr5egkAraxkGorbJb4RxU=
github.com/goreleaser/nfpm/v2 v2.20.0 h1:Q/CrX54KUMluz6+M/pjTbknFd5Dao8qXi0C6ZuFCtfY=
github.com/goreleaser/nfpm/v2 v2.20.0/go.mod h1:/Fh6XfwT/T+D4qtNC2iXmHSD/1UT20JkvBXyJ6nFmOY=
github.com/goreleaser/nfpm/v2 v2.33.0 h1:yBv6jgkPwih4va/S42rceSjJ2Znt3Og/Ntc76oP0tfI=
github.com/goreleaser/nfpm/v2 v2.33.0/go.mod h1:8wwWWvJWmn84xo/Sqiv0aMvEGTHlHZTXTEuVSgQpkIM=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kevinburke/ssh_config v1.1.0 h1:pH/t1WS9NzT8go394IqZeJTMHVm6Cr6ZJ6AQ+mdNo/o=
github.com/kevinburke/ssh_config v1.1.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c=
github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/archiver/v4 v4.0.0-alpha.7 h1:xzByj8G8tj0Oq7ZYYU4+ixL/CVb5ruWCm0EZQ1PjOkE=
github.com/mholt/archiver/v4 v4.0.0-alpha.7/go.mod h1:Fs8qUkO74HHaidabihzYephJH8qmGD/nCP6tE5xC9BM=
github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM=
github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60VhnHxb2X3T1teMyv5A=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE=
github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM=
github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
github.com/smartystreets/assertions v1.13.1 h1:Ef7KhSmjZcK6AVf9YbJdvPYG9avaF0ZxudX+ThRdWfU=
github.com/smartystreets/assertions v1.13.1/go.mod h1:cXr/IwVfSo/RbCSPhoAPv73p3hlSdrBH/b3SdnW/LMY=
github.com/smartystreets/goconvey v1.8.0 h1:Oi49ha/2MURE0WexF052Z0m+BNSGirfjg5RL+JXWq3w=
github.com/smartystreets/goconvey v1.8.0/go.mod h1:EdX8jtrTIj26jmjCOVNMVSIYAtgexqXKHOXW2Dx9JLg=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli/v2 v2.16.3 h1:gHoFIwpPjoyIMbJp/VFd+/vuD0dAgFK4B6DpEMFJfQk=
github.com/urfave/cli/v2 v2.16.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
github.com/xanzy/ssh-agent v0.3.1 h1:AmzO1SSWxw73zxFZPRwaMN1MohDw8UyHnmuxyceTEGo=
github.com/xanzy/ssh-agent v0.3.1/go.mod h1:QIE4lCeL7nkC25x+yA3LBIYfwCc1TFziCtG7cBAac6w=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8=
gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0=
go.arsenm.dev/logger v0.0.0-20220630204155-5ba23e583f0a h1:9SF7Af1OnN9BlWk9yNYCZuv/uAhDMhzfZxtWJEqy6EE=
go.arsenm.dev/logger v0.0.0-20220630204155-5ba23e583f0a/go.mod h1:RV2qydKDdoyaRkhAq8JEGvojR8eJ6bjq5WnSIlH7gYw=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b h1:SCE/18RnFsLrjydh/R/s5EVvHoZprqEQUuoxK8q2Pc4=
golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
go.elara.ws/logger v0.0.0-20230421022458-e80700db2090 h1:RVC8XvWo6Yw4HUshqx4TSzuBDScDghafU6QFRJ4xPZg=
go.elara.ws/logger v0.0.0-20230421022458-e80700db2090/go.mod h1:qng49owViqsW5Aey93lwBXONw20oGbJIoLVscB16mPM=
go.elara.ws/translate v0.0.0-20230421025926-32ccfcd110e6 h1:4xCBxLPBn3Y2DuIcj8zQ1tQOFLrpu6tEIGUWn/Q6zPM=
go.elara.ws/translate v0.0.0-20230421025926-32ccfcd110e6/go.mod h1:NmfCFqwq7X/aqa/ZVkIysj17JyMEY4Bb5E921kMswNo=
go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4 h1:Ep54XceQlKhcCHl9awG+wWP4kz4kIP3c3Lzw/Gc/zwY=
go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4/go.mod h1:/7PNW7nFnDR5W7UXZVc04gdVLR/wBNgkm33KgIz0OBk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU=
go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
@ -241,8 +569,45 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/v3 v3.5.1 h1:hmP3UOw4f+EYexsJjFxvU38+kn+V/s2CclXHanIBkmQ=
mvdan.cc/sh/v3 v3.5.1/go.mod h1:1JcoyAKm1lZw/2bZje/iYKWicU/KMd0rsyJeKHnsK4E=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
lure.sh/fakeroot v0.0.0-20231024000108-b130d64a68ee h1:kSXIuMid56Q29WEl7EQb5QUtmGqQqAu66EZ2G0OSUfU=
lure.sh/fakeroot v0.0.0-20231024000108-b130d64a68ee/go.mod h1:/v0u0AZ+wbzUWhV02KzciOf1KFNh7/7rbkz5Z0b5gDA=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o=
modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA=
modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg=
mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

86
helper.go Normal file
View File

@ -0,0 +1,86 @@
package main
import (
"fmt"
"os"
"strings"
"github.com/urfave/cli/v2"
"lure.sh/lure/internal/cpu"
"lure.sh/lure/internal/shutils/helpers"
"lure.sh/lure/pkg/distro"
"lure.sh/lure/pkg/loggerctx"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
)
var helperCmd = &cli.Command{
Name: "helper",
Usage: "Run a LURE helper command",
ArgsUsage: `<helper_name|"list">`,
Subcommands: []*cli.Command{helperListCmd},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "dest-dir",
Aliases: []string{"d"},
Usage: "The directory that the install commands will install to",
Value: "dest",
},
},
Action: func(c *cli.Context) error {
ctx := c.Context
log := loggerctx.From(ctx)
if c.Args().Len() < 1 {
cli.ShowSubcommandHelpAndExit(c, 1)
}
helper, ok := helpers.Helpers[c.Args().First()]
if !ok {
log.Fatal("No such helper command").Str("name", c.Args().First()).Send()
}
wd, err := os.Getwd()
if err != nil {
log.Fatal("Error getting working directory").Err(err).Send()
}
info, err := distro.ParseOSRelease(ctx)
if err != nil {
log.Fatal("Error getting working directory").Err(err).Send()
}
hc := interp.HandlerContext{
Env: expand.ListEnviron(
"pkgdir="+c.String("dest-dir"),
"DISTRO_ID="+info.ID,
"DISTRO_ID_LIKE="+strings.Join(info.Like, " "),
"ARCH="+cpu.Arch(),
),
Dir: wd,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
}
return helper(hc, c.Args().First(), c.Args().Slice()[1:])
},
CustomHelpTemplate: cli.CommandHelpTemplate,
BashComplete: func(ctx *cli.Context) {
for name := range helpers.Helpers {
fmt.Println(name)
}
},
}
var helperListCmd = &cli.Command{
Name: "list",
Usage: "List all the available helper commands",
Aliases: []string{"ls"},
Action: func(ctx *cli.Context) error {
for name := range helpers.Helpers {
fmt.Println(name)
}
return nil
},
}

99
info.go
View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -19,41 +19,88 @@
package main
import (
"fmt"
"os"
"github.com/urfave/cli/v2"
"go.arsenm.dev/lure/distro"
"lure.sh/lure/internal/cliutils"
"lure.sh/lure/internal/config"
"lure.sh/lure/internal/overrides"
"lure.sh/lure/pkg/distro"
"lure.sh/lure/pkg/loggerctx"
"lure.sh/lure/pkg/repos"
"gopkg.in/yaml.v3"
)
func infoCmd(c *cli.Context) error {
args := c.Args()
if args.Len() < 1 {
log.Fatalf("Command info expected at least 1 argument, got %d", args.Len()).Send()
}
var infoCmd = &cli.Command{
Name: "info",
Usage: "Print information about a package",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "all",
Aliases: []string{"a"},
Usage: "Show all information, not just for the current distro",
},
},
Action: func(c *cli.Context) error {
ctx := c.Context
log := loggerctx.From(ctx)
info, err := distro.ParseOSRelease(c.Context)
if err != nil {
log.Fatal("Error parsing os-release").Err(err).Send()
}
args := c.Args()
if args.Len() < 1 {
log.Fatalf("Command info expected at least 1 argument, got %d", args.Len()).Send()
}
found, err := findPkg(args.First())
if err != nil {
log.Fatal("Error finding package").Err(err).Send()
}
err := repos.Pull(ctx, config.Config(ctx).Repos)
if err != nil {
log.Fatal("Error pulling repositories").Err(err).Send()
}
// if multiple are matched, only use the first one
script := found[0]
found, _, err := repos.FindPkgs(ctx, args.Slice())
if err != nil {
log.Fatal("Error finding packages").Err(err).Send()
}
vars, err := getBuildVars(c.Context, script, info)
if err != nil {
log.Fatal("Error getting build variables").Err(err).Send()
}
if len(found) == 0 {
os.Exit(1)
}
err = yaml.NewEncoder(os.Stdout).Encode(vars)
if err != nil {
log.Fatal("Error encoding script variables").Err(err).Send()
}
pkgs := cliutils.FlattenPkgs(ctx, found, "show", c.Bool("interactive"))
return nil
var names []string
all := c.Bool("all")
if !all {
info, err := distro.ParseOSRelease(ctx)
if err != nil {
log.Fatal("Error parsing os-release file").Err(err).Send()
}
names, err = overrides.Resolve(
info,
overrides.DefaultOpts.
WithLanguages([]string{config.SystemLang()}),
)
if err != nil {
log.Fatal("Error resolving overrides").Err(err).Send()
}
}
for _, pkg := range pkgs {
if !all {
err = yaml.NewEncoder(os.Stdout).Encode(overrides.ResolvePackage(&pkg, names))
if err != nil {
log.Fatal("Error encoding script variables").Err(err).Send()
}
} else {
err = yaml.NewEncoder(os.Stdout).Encode(pkg)
if err != nil {
log.Fatal("Error encoding script variables").Err(err).Send()
}
}
fmt.Println("---")
}
return nil
},
}

View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -19,77 +19,104 @@
package main
import (
"context"
"fmt"
"github.com/urfave/cli/v2"
"go.arsenm.dev/lure/manager"
"lure.sh/lure/internal/cliutils"
"lure.sh/lure/internal/config"
"lure.sh/lure/internal/db"
"lure.sh/lure/internal/types"
"lure.sh/lure/pkg/build"
"lure.sh/lure/pkg/loggerctx"
"lure.sh/lure/pkg/manager"
"lure.sh/lure/pkg/repos"
)
func installCmd(c *cli.Context) error {
args := c.Args()
if args.Len() < 1 {
log.Fatalf("Command install expected at least 1 argument, got %d", args.Len()).Send()
}
var installCmd = &cli.Command{
Name: "install",
Usage: "Install a new package",
Aliases: []string{"in"},
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "clean",
Aliases: []string{"c"},
Usage: "Build package from scratch even if there's an already built package available",
},
},
Action: func(c *cli.Context) error {
ctx := c.Context
log := loggerctx.From(ctx)
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect supported package manager on system").Send()
}
args := c.Args()
if args.Len() < 1 {
log.Fatalf("Command install expected at least 1 argument, got %d", args.Len()).Send()
}
installPkgs(c.Context, args.Slice(), mgr, true)
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect a supported package manager on the system").Send()
}
return nil
}
func installPkgs(ctx context.Context, pkgs []string, mgr manager.Manager, pull bool) {
if pull {
err := pullRepos(ctx)
err := repos.Pull(ctx, config.Config(ctx).Repos)
if err != nil {
log.Fatal("Error pulling repositories").Err(err).Send()
}
}
scripts, notFound := findPkgs(pkgs)
if len(notFound) > 0 {
err := mgr.Install(nil, notFound...)
found, notFound, err := repos.FindPkgs(ctx, args.Slice())
if err != nil {
log.Fatal("Error installing native packages").Err(err).Send()
}
}
installScripts(ctx, mgr, scripts)
}
func installScripts(ctx context.Context, mgr manager.Manager, scripts []string) {
for _, script := range scripts {
builtPkgs, _, err := buildPackage(ctx, script, mgr)
if err != nil {
log.Fatal("Error building package").Err(err).Send()
log.Fatal("Error finding packages").Err(err).Send()
}
err = mgr.InstallLocal(nil, builtPkgs...)
pkgs := cliutils.FlattenPkgs(ctx, found, "install", c.Bool("interactive"))
build.InstallPkgs(ctx, pkgs, notFound, types.BuildOpts{
Manager: mgr,
Clean: c.Bool("clean"),
Interactive: c.Bool("interactive"),
})
return nil
},
BashComplete: func(c *cli.Context) {
log := loggerctx.From(c.Context)
result, err := db.GetPkgs(c.Context, "true")
if err != nil {
log.Fatal("Error installing package").Err(err).Send()
log.Fatal("Error getting packages").Err(err).Send()
}
}
defer result.Close()
for result.Next() {
var pkg db.Package
err = result.StructScan(&pkg)
if err != nil {
log.Fatal("Error iterating over packages").Err(err).Send()
}
fmt.Println(pkg.Name)
}
},
}
func removeCmd(c *cli.Context) error {
args := c.Args()
if args.Len() < 1 {
log.Fatalf("Command remove expected at least 1 argument, got %d", args.Len()).Send()
}
var removeCmd = &cli.Command{
Name: "remove",
Usage: "Remove an installed package",
Aliases: []string{"rm"},
Action: func(c *cli.Context) error {
log := loggerctx.From(c.Context)
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect supported package manager on system").Send()
}
args := c.Args()
if args.Len() < 1 {
log.Fatalf("Command remove expected at least 1 argument, got %d", args.Len()).Send()
}
err := mgr.Remove(nil, c.Args().Slice()...)
if err != nil {
log.Fatal("Error removing packages").Err(err).Send()
}
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect a supported package manager on the system").Send()
}
return nil
err := mgr.Remove(nil, c.Args().Slice()...)
if err != nil {
log.Fatal("Error removing packages").Err(err).Send()
}
return nil
},
}

172
internal/cliutils/prompt.go Normal file
View File

@ -0,0 +1,172 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cliutils
import (
"context"
"os"
"strings"
"github.com/AlecAivazis/survey/v2"
"lure.sh/lure/internal/config"
"lure.sh/lure/internal/db"
"lure.sh/lure/internal/pager"
"lure.sh/lure/internal/translations"
"lure.sh/lure/pkg/loggerctx"
)
// YesNoPrompt asks the user a yes or no question, using def as the default answer
func YesNoPrompt(ctx context.Context, msg string, interactive, def bool) (bool, error) {
if interactive {
var answer bool
err := survey.AskOne(
&survey.Confirm{
Message: translations.Translator(ctx).TranslateTo(msg, config.Language(ctx)),
Default: def,
},
&answer,
)
return answer, err
} else {
return def, nil
}
}
// PromptViewScript asks the user if they'd like to see a script,
// shows it if they answer yes, then asks if they'd still like to
// continue, and exits if they answer no.
func PromptViewScript(ctx context.Context, script, name, style string, interactive bool) error {
log := loggerctx.From(ctx)
if !interactive {
return nil
}
scriptPrompt := translations.Translator(ctx).TranslateTo("Would you like to view the build script for", config.Language(ctx)) + " " + name
view, err := YesNoPrompt(ctx, scriptPrompt, interactive, false)
if err != nil {
return err
}
if view {
err = ShowScript(script, name, style)
if err != nil {
return err
}
cont, err := YesNoPrompt(ctx, "Would you still like to continue?", interactive, false)
if err != nil {
return err
}
if !cont {
log.Fatal(translations.Translator(ctx).TranslateTo("User chose not to continue after reading script", config.Language(ctx))).Send()
}
}
return nil
}
// ShowScript uses the built-in pager to display a script at a
// given path, in the given syntax highlighting style.
func ShowScript(path, name, style string) error {
scriptFl, err := os.Open(path)
if err != nil {
return err
}
defer scriptFl.Close()
str, err := pager.SyntaxHighlightBash(scriptFl, style)
if err != nil {
return err
}
pgr := pager.New(name, str)
return pgr.Run()
}
// FlattenPkgs attempts to flatten the a map of slices of packages into a single slice
// of packages by prompting the user if multiple packages match.
func FlattenPkgs(ctx context.Context, found map[string][]db.Package, verb string, interactive bool) []db.Package {
log := loggerctx.From(ctx)
var outPkgs []db.Package
for _, pkgs := range found {
if len(pkgs) > 1 && interactive {
choice, err := PkgPrompt(ctx, pkgs, verb, interactive)
if err != nil {
log.Fatal("Error prompting for choice of package").Send()
}
outPkgs = append(outPkgs, choice)
} else if len(pkgs) == 1 || !interactive {
outPkgs = append(outPkgs, pkgs[0])
}
}
return outPkgs
}
// PkgPrompt asks the user to choose between multiple packages.
func PkgPrompt(ctx context.Context, options []db.Package, verb string, interactive bool) (db.Package, error) {
if !interactive {
return options[0], nil
}
names := make([]string, len(options))
for i, option := range options {
names[i] = option.Repository + "/" + option.Name + " " + option.Version
}
prompt := &survey.Select{
Options: names,
Message: translations.Translator(ctx).TranslateTo("Choose which package to "+verb, config.Language(ctx)),
}
var choice int
err := survey.AskOne(prompt, &choice)
if err != nil {
return db.Package{}, err
}
return options[choice], nil
}
// ChooseOptDepends asks the user to choose between multiple optional dependencies.
// The user may choose multiple items.
func ChooseOptDepends(ctx context.Context, options []string, verb string, interactive bool) ([]string, error) {
if !interactive {
return []string{}, nil
}
prompt := &survey.MultiSelect{
Options: options,
Message: translations.Translator(ctx).TranslateTo("Choose which optional package(s) to install", config.Language(ctx)),
}
var choices []int
err := survey.AskOne(prompt, &choices)
if err != nil {
return nil, err
}
out := make([]string, len(choices))
for i, choiceIndex := range choices {
out[i], _, _ = strings.Cut(options[choiceIndex], ": ")
}
return out, nil
}

79
internal/config/config.go Normal file
View File

@ -0,0 +1,79 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package config
import (
"context"
"os"
"sync"
"github.com/pelletier/go-toml/v2"
"lure.sh/lure/internal/types"
"lure.sh/lure/pkg/loggerctx"
)
var defaultConfig = &types.Config{
RootCmd: "sudo",
PagerStyle: "native",
IgnorePkgUpdates: []string{},
Repos: []types.Repo{
{
Name: "default",
URL: "https://github.com/lure-sh/lure-repo.git",
},
},
}
var (
configMtx sync.Mutex
config *types.Config
)
// Config returns a LURE configuration struct.
// The first time it's called, it'll load the config from a file.
// Subsequent calls will just return the same value.
func Config(ctx context.Context) *types.Config {
configMtx.Lock()
defer configMtx.Unlock()
log := loggerctx.From(ctx)
if config == nil {
cfgFl, err := os.Open(GetPaths(ctx).ConfigPath)
if err != nil {
log.Warn("Error opening config file, using defaults").Err(err).Send()
return defaultConfig
}
defer cfgFl.Close()
// Copy the default configuration into config
defCopy := *defaultConfig
config = &defCopy
config.Repos = nil
err = toml.NewDecoder(cfgFl).Decode(config)
if err != nil {
log.Warn("Error decoding config file, using defaults").Err(err).Send()
// Set config back to nil so that we try again next time
config = nil
return defaultConfig
}
}
return config
}

67
internal/config/lang.go Normal file
View File

@ -0,0 +1,67 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package config
import (
"context"
"os"
"strings"
"sync"
"lure.sh/lure/pkg/loggerctx"
"golang.org/x/text/language"
)
var (
langMtx sync.Mutex
lang language.Tag
langSet bool
)
// Language returns the system language.
// The first time it's called, it'll detect the langauge based on
// the $LANG environment variable.
// Subsequent calls will just return the same value.
func Language(ctx context.Context) language.Tag {
langMtx.Lock()
defer langMtx.Unlock()
log := loggerctx.From(ctx)
if !langSet {
syslang := SystemLang()
tag, err := language.Parse(syslang)
if err != nil {
log.Fatal("Error parsing system language").Err(err).Send()
}
base, _ := tag.Base()
lang = language.Make(base.String())
langSet = true
}
return lang
}
// SystemLang returns the system language based on
// the $LANG environment variable.
func SystemLang() string {
lang := os.Getenv("LANG")
lang, _, _ = strings.Cut(lang, ".")
if lang == "" || lang == "C" {
lang = "en"
}
return lang
}

108
internal/config/paths.go Normal file
View File

@ -0,0 +1,108 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package config
import (
"context"
"os"
"path/filepath"
"sync"
"github.com/pelletier/go-toml/v2"
"lure.sh/lure/pkg/loggerctx"
)
// Paths contains various paths used by LURE
type Paths struct {
ConfigDir string
ConfigPath string
CacheDir string
RepoDir string
PkgsDir string
DBPath string
}
var (
pathsMtx sync.Mutex
paths *Paths
)
// GetPaths returns a Paths struct.
// The first time it's called, it'll generate the struct
// using information from the system.
// Subsequent calls will return the same value.
func GetPaths(ctx context.Context) *Paths {
pathsMtx.Lock()
defer pathsMtx.Unlock()
log := loggerctx.From(ctx)
if paths == nil {
paths = &Paths{}
cfgDir, err := os.UserConfigDir()
if err != nil {
log.Fatal("Unable to detect user config directory").Err(err).Send()
}
paths.ConfigDir = filepath.Join(cfgDir, "lure")
err = os.MkdirAll(paths.ConfigDir, 0o755)
if err != nil {
log.Fatal("Unable to create LURE config directory").Err(err).Send()
}
paths.ConfigPath = filepath.Join(paths.ConfigDir, "lure.toml")
if _, err := os.Stat(paths.ConfigPath); err != nil {
cfgFl, err := os.Create(paths.ConfigPath)
if err != nil {
log.Fatal("Unable to create LURE config file").Err(err).Send()
}
err = toml.NewEncoder(cfgFl).Encode(&defaultConfig)
if err != nil {
log.Fatal("Error encoding default configuration").Err(err).Send()
}
cfgFl.Close()
}
cacheDir, err := os.UserCacheDir()
if err != nil {
log.Fatal("Unable to detect cache directory").Err(err).Send()
}
paths.CacheDir = filepath.Join(cacheDir, "lure")
paths.RepoDir = filepath.Join(paths.CacheDir, "repo")
paths.PkgsDir = filepath.Join(paths.CacheDir, "pkgs")
err = os.MkdirAll(paths.RepoDir, 0o755)
if err != nil {
log.Fatal("Unable to create repo cache directory").Err(err).Send()
}
err = os.MkdirAll(paths.PkgsDir, 0o755)
if err != nil {
log.Fatal("Unable to create package cache directory").Err(err).Send()
}
paths.DBPath = filepath.Join(paths.CacheDir, "db")
}
return paths
}

View File

@ -0,0 +1,5 @@
package config
// Version contains the version of LURE. If the version
// isn't known, it'll be set to "unknown"
var Version = "unknown"

View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -20,14 +20,17 @@ package cpu
import (
"os"
"runtime"
"strconv"
"strings"
"golang.org/x/exp/slices"
"golang.org/x/sys/cpu"
)
// ARMVariant checks which variant of ARM lure is running
// armVariant checks which variant of ARM lure is running
// on, by using the same detection method as Go itself
func ARMVariant() string {
func armVariant() string {
armEnv := os.Getenv("LURE_ARM_VARIANT")
// ensure value has "arm" prefix, such as arm5 or arm6
if strings.HasPrefix(armEnv, "arm") {
@ -42,3 +45,73 @@ func ARMVariant() string {
return "arm5"
}
}
// Arch returns the canonical CPU architecture of the system
func Arch() string {
arch := os.Getenv("LURE_ARCH")
if arch == "" {
arch = runtime.GOARCH
}
if arch == "arm" {
arch = armVariant()
}
return arch
}
func IsCompatibleWith(target string, list []string) bool {
if target == "all" || slices.Contains(list, "all") {
return true
}
for _, arch := range list {
if strings.HasPrefix(target, "arm") && strings.HasPrefix(arch, "arm") {
targetVer, err := getARMVersion(target)
if err != nil {
return false
}
archVer, err := getARMVersion(arch)
if err != nil {
return false
}
if targetVer >= archVer {
return true
}
}
if target == arch {
return true
}
}
return false
}
func CompatibleArches(arch string) ([]string, error) {
if strings.HasPrefix(arch, "arm") {
ver, err := getARMVersion(arch)
if err != nil {
return nil, err
}
if ver > 5 {
var out []string
for i := ver; i >= 5; i-- {
out = append(out, "arm"+strconv.Itoa(i))
}
return out, nil
}
}
return []string{arch}, nil
}
func getARMVersion(arch string) (int, error) {
// Extract the version number from ARM architecture
version := strings.TrimPrefix(arch, "arm")
if version == "" {
return 5, nil // Default to arm5 if version is not specified
}
return strconv.Atoi(version)
}

346
internal/db/db.go Normal file
View File

@ -0,0 +1,346 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package db
import (
"context"
"database/sql"
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"sync"
"github.com/jmoiron/sqlx"
"lure.sh/lure/internal/config"
"lure.sh/lure/pkg/loggerctx"
"golang.org/x/exp/slices"
"modernc.org/sqlite"
)
// CurrentVersion is the current version of the database.
// The database is reset if its version doesn't match this.
const CurrentVersion = 2
func init() {
sqlite.MustRegisterScalarFunction("json_array_contains", 2, jsonArrayContains)
}
// Package is a LURE package's database representation
type Package struct {
Name string `sh:"name,required" db:"name"`
Version string `sh:"version,required" db:"version"`
Release int `sh:"release,required" db:"release"`
Epoch uint `sh:"epoch" db:"epoch"`
Description JSON[map[string]string] `db:"description"`
Homepage JSON[map[string]string] `db:"homepage"`
Maintainer JSON[map[string]string] `db:"maintainer"`
Architectures JSON[[]string] `sh:"architectures" db:"architectures"`
Licenses JSON[[]string] `sh:"license" db:"licenses"`
Provides JSON[[]string] `sh:"provides" db:"provides"`
Conflicts JSON[[]string] `sh:"conflicts" db:"conflicts"`
Replaces JSON[[]string] `sh:"replaces" db:"replaces"`
Depends JSON[map[string][]string] `db:"depends"`
BuildDepends JSON[map[string][]string] `db:"builddepends"`
OptDepends JSON[map[string][]string] `db:"optdepends"`
Repository string `db:"repository"`
}
type version struct {
Version int `db:"version"`
}
var (
mu sync.Mutex
conn *sqlx.DB
closed = true
)
// DB returns the LURE database.
// The first time it's called, it opens the SQLite database file.
// Subsequent calls return the same connection.
func DB(ctx context.Context) *sqlx.DB {
log := loggerctx.From(ctx)
if conn != nil && !closed {
return getConn()
}
_, err := open(ctx, config.GetPaths(ctx).DBPath)
if err != nil {
log.Fatal("Error opening database").Err(err).Send()
}
return getConn()
}
func getConn() *sqlx.DB {
mu.Lock()
defer mu.Unlock()
return conn
}
func open(ctx context.Context, dsn string) (*sqlx.DB, error) {
db, err := sqlx.Open("sqlite", dsn)
if err != nil {
return nil, err
}
mu.Lock()
conn = db
closed = false
mu.Unlock()
err = initDB(ctx, dsn)
if err != nil {
return nil, err
}
return db, nil
}
// Close closes the database
func Close() error {
closed = true
if conn != nil {
return conn.Close()
} else {
return nil
}
}
// initDB initializes the database
func initDB(ctx context.Context, dsn string) error {
log := loggerctx.From(ctx)
conn = conn.Unsafe()
_, err := conn.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS pkgs (
name TEXT NOT NULL,
repository TEXT NOT NULL,
version TEXT NOT NULL,
release INT NOT NULL,
epoch INT,
description TEXT CHECK(description = 'null' OR (JSON_VALID(description) AND JSON_TYPE(description) = 'object')),
homepage TEXT CHECK(homepage = 'null' OR (JSON_VALID(homepage) AND JSON_TYPE(homepage) = 'object')),
maintainer TEXT CHECK(maintainer = 'null' OR (JSON_VALID(maintainer) AND JSON_TYPE(maintainer) = 'object')),
architectures TEXT CHECK(architectures = 'null' OR (JSON_VALID(architectures) AND JSON_TYPE(architectures) = 'array')),
licenses TEXT CHECK(licenses = 'null' OR (JSON_VALID(licenses) AND JSON_TYPE(licenses) = 'array')),
provides TEXT CHECK(provides = 'null' OR (JSON_VALID(provides) AND JSON_TYPE(provides) = 'array')),
conflicts TEXT CHECK(conflicts = 'null' OR (JSON_VALID(conflicts) AND JSON_TYPE(conflicts) = 'array')),
replaces TEXT CHECK(replaces = 'null' OR (JSON_VALID(replaces) AND JSON_TYPE(replaces) = 'array')),
depends TEXT CHECK(depends = 'null' OR (JSON_VALID(depends) AND JSON_TYPE(depends) = 'object')),
builddepends TEXT CHECK(builddepends = 'null' OR (JSON_VALID(builddepends) AND JSON_TYPE(builddepends) = 'object')),
optdepends TEXT CHECK(optdepends = 'null' OR (JSON_VALID(optdepends) AND JSON_TYPE(optdepends) = 'object')),
UNIQUE(name, repository)
);
CREATE TABLE IF NOT EXISTS lure_db_version (
version INT NOT NULL
);
`)
if err != nil {
return err
}
ver, ok := GetVersion(ctx)
if ok && ver != CurrentVersion {
log.Warn("Database version mismatch; resetting").Int("version", ver).Int("expected", CurrentVersion).Send()
reset(ctx)
return initDB(ctx, dsn)
} else if !ok {
log.Warn("Database version does not exist. Run lure fix if something isn't working.").Send()
return addVersion(ctx, CurrentVersion)
}
return nil
}
// reset drops all the database tables
func reset(ctx context.Context) error {
_, err := DB(ctx).ExecContext(ctx, "DROP TABLE IF EXISTS pkgs;")
if err != nil {
return err
}
_, err = DB(ctx).ExecContext(ctx, "DROP TABLE IF EXISTS lure_db_version;")
return err
}
// IsEmpty returns true if the database has no packages in it, otherwise it returns false.
func IsEmpty(ctx context.Context) bool {
var count int
err := DB(ctx).GetContext(ctx, &count, "SELECT count(1) FROM pkgs;")
if err != nil {
return true
}
return count == 0
}
// GetVersion returns the database version and a boolean indicating
// whether the database contained a version number
func GetVersion(ctx context.Context) (int, bool) {
var ver version
err := DB(ctx).GetContext(ctx, &ver, "SELECT * FROM lure_db_version LIMIT 1;")
if err != nil {
return 0, false
}
return ver.Version, true
}
func addVersion(ctx context.Context, ver int) error {
_, err := DB(ctx).ExecContext(ctx, `INSERT INTO lure_db_version(version) VALUES (?);`, ver)
return err
}
// InsertPackage adds a package to the database
func InsertPackage(ctx context.Context, pkg Package) error {
_, err := DB(ctx).NamedExecContext(ctx, `
INSERT OR REPLACE INTO pkgs (
name,
repository,
version,
release,
epoch,
description,
homepage,
maintainer,
architectures,
licenses,
provides,
conflicts,
replaces,
depends,
builddepends,
optdepends
) VALUES (
:name,
:repository,
:version,
:release,
:epoch,
:description,
:homepage,
:maintainer,
:architectures,
:licenses,
:provides,
:conflicts,
:replaces,
:depends,
:builddepends,
:optdepends
);
`, pkg)
return err
}
// GetPkgs returns a result containing packages that match the where conditions
func GetPkgs(ctx context.Context, where string, args ...any) (*sqlx.Rows, error) {
stream, err := DB(ctx).QueryxContext(ctx, "SELECT * FROM pkgs WHERE "+where, args...)
if err != nil {
return nil, err
}
return stream, nil
}
// GetPkg returns a single package that matches the where conditions
func GetPkg(ctx context.Context, where string, args ...any) (*Package, error) {
out := &Package{}
err := DB(ctx).GetContext(ctx, out, "SELECT * FROM pkgs WHERE "+where+" LIMIT 1", args...)
return out, err
}
// DeletePkgs deletes all packages matching the where conditions
func DeletePkgs(ctx context.Context, where string, args ...any) error {
_, err := DB(ctx).ExecContext(ctx, "DELETE FROM pkgs WHERE "+where, args...)
return err
}
// jsonArrayContains is an SQLite function that checks if a JSON array
// in the database contains a given value
func jsonArrayContains(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value, error) {
value, ok := args[0].(string)
if !ok {
return nil, errors.New("both arguments to json_array_contains must be strings")
}
item, ok := args[1].(string)
if !ok {
return nil, errors.New("both arguments to json_array_contains must be strings")
}
var array []string
err := json.Unmarshal([]byte(value), &array)
if err != nil {
return nil, err
}
return slices.Contains(array, item), nil
}
// JSON represents a JSON value in the database
type JSON[T any] struct {
Val T
}
// NewJSON creates a new database JSON value
func NewJSON[T any](v T) JSON[T] {
return JSON[T]{Val: v}
}
func (s *JSON[T]) Scan(val any) error {
if val == nil {
return nil
}
switch val := val.(type) {
case string:
err := json.Unmarshal([]byte(val), &s.Val)
if err != nil {
return err
}
case sql.NullString:
if val.Valid {
err := json.Unmarshal([]byte(val.String), &s.Val)
if err != nil {
return err
}
}
default:
return errors.New("sqlite json types must be strings")
}
return nil
}
func (s JSON[T]) Value() (driver.Value, error) {
data, err := json.Marshal(s.Val)
if err != nil {
return nil, err
}
return string(data), nil
}
func (s JSON[T]) MarshalYAML() (any, error) {
return s.Val, nil
}
func (s JSON[T]) String() string {
return fmt.Sprint(s.Val)
}
func (s JSON[T]) GoString() string {
return fmt.Sprintf("%#v", s.Val)
}

250
internal/db/db_test.go Normal file
View File

@ -0,0 +1,250 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package db_test
import (
"reflect"
"strings"
"testing"
"github.com/jmoiron/sqlx"
"lure.sh/lure/internal/db"
)
var testPkg = db.Package{
Name: "test",
Version: "0.0.1",
Release: 1,
Epoch: 2,
Description: db.NewJSON(map[string]string{
"en": "Test package",
"ru": "Проверочный пакет",
}),
Homepage: db.NewJSON(map[string]string{
"en": "https://lure.sh/",
}),
Maintainer: db.NewJSON(map[string]string{
"en": "Elara Musayelyan <elara@elara.ws>",
"ru": "Элара Мусаелян <elara@elara.ws>",
}),
Architectures: db.NewJSON([]string{"arm64", "amd64"}),
Licenses: db.NewJSON([]string{"GPL-3.0-or-later"}),
Provides: db.NewJSON([]string{"test"}),
Conflicts: db.NewJSON([]string{"test"}),
Replaces: db.NewJSON([]string{"test-old"}),
Depends: db.NewJSON(map[string][]string{
"": {"sudo"},
}),
BuildDepends: db.NewJSON(map[string][]string{
"": {"golang"},
"arch": {"go"},
}),
Repository: "default",
}
func TestInit(t *testing.T) {
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer db.Close()
_, err = db.DB().Exec("SELECT * FROM pkgs")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
ver, ok := db.GetVersion()
if !ok {
t.Errorf("Expected version to be present")
} else if ver != db.CurrentVersion {
t.Errorf("Expected version %d, got %d", db.CurrentVersion, ver)
}
}
func TestInsertPackage(t *testing.T) {
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer db.Close()
err = db.InsertPackage(testPkg)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
dbPkg := db.Package{}
err = sqlx.Get(db.DB(), &dbPkg, "SELECT * FROM pkgs WHERE name = 'test' AND repository = 'default'")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
if !reflect.DeepEqual(testPkg, dbPkg) {
t.Errorf("Expected test package to be the same as database package")
}
}
func TestGetPkgs(t *testing.T) {
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer db.Close()
x1 := testPkg
x1.Name = "x1"
x2 := testPkg
x2.Name = "x2"
err = db.InsertPackage(x1)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
err = db.InsertPackage(x2)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
result, err := db.GetPkgs("name LIKE 'x%'")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
for result.Next() {
var dbPkg db.Package
err = result.StructScan(&dbPkg)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
if !strings.HasPrefix(dbPkg.Name, "x") {
t.Errorf("Expected package name to start with 'x', got %s", dbPkg.Name)
}
}
}
func TestGetPkg(t *testing.T) {
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer db.Close()
x1 := testPkg
x1.Name = "x1"
x2 := testPkg
x2.Name = "x2"
err = db.InsertPackage(x1)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
err = db.InsertPackage(x2)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
pkg, err := db.GetPkg("name LIKE 'x%' ORDER BY name")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
if pkg.Name != "x1" {
t.Errorf("Expected x1 package, got %s", pkg.Name)
}
if !reflect.DeepEqual(*pkg, x1) {
t.Errorf("Expected x1 to be %v, got %v", x1, *pkg)
}
}
func TestDeletePkgs(t *testing.T) {
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer db.Close()
x1 := testPkg
x1.Name = "x1"
x2 := testPkg
x2.Name = "x2"
err = db.InsertPackage(x1)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
err = db.InsertPackage(x2)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
err = db.DeletePkgs("name = 'x1'")
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
var dbPkg db.Package
err = db.DB().Get(&dbPkg, "SELECT * FROM pkgs WHERE name LIKE 'x%' ORDER BY name LIMIT 1;")
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
if dbPkg.Name != "x2" {
t.Errorf("Expected x2 package, got %s", dbPkg.Name)
}
}
func TestJsonArrayContains(t *testing.T) {
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer db.Close()
x1 := testPkg
x1.Name = "x1"
x2 := testPkg
x2.Name = "x2"
x2.Provides.Val = append(x2.Provides.Val, "x")
err = db.InsertPackage(x1)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
err = db.InsertPackage(x2)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
var dbPkg db.Package
err = db.DB().Get(&dbPkg, "SELECT * FROM pkgs WHERE json_array_contains(provides, 'x');")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
if dbPkg.Name != "x2" {
t.Errorf("Expected x2 package, got %s", dbPkg.Name)
}
}

379
internal/dl/dl.go Normal file
View File

@ -0,0 +1,379 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Package dl contains abstractions for downloadingfiles and directories
// from various sources.
package dl
import (
"context"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"errors"
"fmt"
"hash"
"io"
"os"
"path/filepath"
"strings"
"github.com/PuerkitoBio/purell"
"github.com/vmihailenco/msgpack/v5"
"golang.org/x/crypto/blake2b"
"golang.org/x/crypto/blake2s"
"golang.org/x/exp/slices"
"lure.sh/lure/internal/dlcache"
"lure.sh/lure/pkg/loggerctx"
)
const manifestFileName = ".lure_cache_manifest"
// ErrChecksumMismatch occurs when the checksum of a downloaded file
// does not match the expected checksum provided in the Options struct.
var (
ErrChecksumMismatch = errors.New("dl: checksums did not match")
ErrNoSuchHashAlgo = errors.New("dl: invalid hashing algorithm")
)
// Downloaders contains all the downloaders in the order in which
// they should be checked
var Downloaders = []Downloader{
GitDownloader{},
TorrentDownloader{},
FileDownloader{},
}
// Type represents the type of download (file or directory)
type Type uint8
const (
TypeFile Type = iota
TypeDir
)
func (t Type) String() string {
switch t {
case TypeFile:
return "file"
case TypeDir:
return "dir"
}
return "<unknown>"
}
// Options contains the options for downloading
// files and directories
type Options struct {
Hash []byte
HashAlgorithm string
Name string
URL string
Destination string
CacheDisabled bool
PostprocDisabled bool
Progress io.Writer
LocalDir string
}
func (opts Options) NewHash() (hash.Hash, error) {
switch opts.HashAlgorithm {
case "", "sha256":
return sha256.New(), nil
case "sha224":
return sha256.New224(), nil
case "sha512":
return sha512.New(), nil
case "sha384":
return sha512.New384(), nil
case "sha1":
return sha1.New(), nil
case "md5":
return md5.New(), nil
case "blake2s-128":
return blake2s.New256(nil)
case "blake2s-256":
return blake2s.New256(nil)
case "blake2b-256":
return blake2b.New(32, nil)
case "blake2b-512":
return blake2b.New(64, nil)
default:
return nil, fmt.Errorf("%w: %s", ErrNoSuchHashAlgo, opts.HashAlgorithm)
}
}
// Manifest holds information about the type and name
// of a downloaded file or directory. It is stored inside
// each cache directory for later use.
type Manifest struct {
Type Type
Name string
}
type Downloader interface {
// Name returns the name of the downloader
Name() string
// MatchURL checks if the given URL matches
// the downloader.
MatchURL(string) bool
// Download downloads the object at the URL
// provided in the options, to the destination
// given in the options. It returns a type,
// a name for the downloaded object (this may be empty),
// and an error.
Download(Options) (Type, string, error)
}
// UpdatingDownloader extends the Downloader interface
// with an Update method for protocols such as git, which
// allow for incremental updates without changing the URL.
type UpdatingDownloader interface {
Downloader
// Update checks for and performs any
// available updates for the object
// described in the options. It returns
// true if an update was performed, or
// false if no update was required.
Update(Options) (bool, error)
}
// Download downloads a file or directory using the specified options.
// It first gets the appropriate downloader for the URL, then checks
// if caching is enabled. If caching is enabled, it attempts to get
// the cache directory for the URL and update it if necessary.
// If the source is found in the cache, it links it to the destination
// using hard links. If the source is not found in the cache,
// it downloads the source to a new cache directory and links it
// to the destination.
func Download(ctx context.Context, opts Options) (err error) {
log := loggerctx.From(ctx)
normalized, err := normalizeURL(opts.URL)
if err != nil {
return err
}
opts.URL = normalized
d := getDownloader(opts.URL)
if opts.CacheDisabled {
_, _, err = d.Download(opts)
return err
}
var t Type
cacheDir, ok := dlcache.Get(ctx, opts.URL)
if ok {
var updated bool
if d, ok := d.(UpdatingDownloader); ok {
log.Info("Source can be updated, updating if required").Str("source", opts.Name).Str("downloader", d.Name()).Send()
updated, err = d.Update(Options{
Hash: opts.Hash,
HashAlgorithm: opts.HashAlgorithm,
Name: opts.Name,
URL: opts.URL,
Destination: cacheDir,
Progress: opts.Progress,
LocalDir: opts.LocalDir,
})
if err != nil {
return err
}
}
m, err := getManifest(cacheDir)
if err == nil {
t = m.Type
dest := filepath.Join(opts.Destination, m.Name)
ok, err := handleCache(cacheDir, dest, m.Name, t)
if err != nil {
return err
}
if ok && !updated {
log.Info("Source found in cache and linked to destination").Str("source", opts.Name).Stringer("type", t).Send()
return nil
} else if ok {
log.Info("Source updated and linked to destination").Str("source", opts.Name).Stringer("type", t).Send()
return nil
}
} else {
// If we cannot read the manifest,
// this cache entry is invalid and
// the source must be re-downloaded.
err = os.RemoveAll(cacheDir)
if err != nil {
return err
}
}
}
log.Info("Downloading source").Str("source", opts.Name).Str("downloader", d.Name()).Send()
cacheDir, err = dlcache.New(ctx, opts.URL)
if err != nil {
return err
}
t, name, err := d.Download(Options{
Hash: opts.Hash,
HashAlgorithm: opts.HashAlgorithm,
Name: opts.Name,
URL: opts.URL,
Destination: cacheDir,
Progress: opts.Progress,
LocalDir: opts.LocalDir,
})
if err != nil {
return err
}
err = writeManifest(cacheDir, Manifest{t, name})
if err != nil {
return err
}
dest := filepath.Join(opts.Destination, name)
_, err = handleCache(cacheDir, dest, name, t)
return err
}
// writeManifest writes the manifest to the specified cache directory.
func writeManifest(cacheDir string, m Manifest) error {
fl, err := os.Create(filepath.Join(cacheDir, manifestFileName))
if err != nil {
return err
}
defer fl.Close()
return msgpack.NewEncoder(fl).Encode(m)
}
// getManifest reads the manifest from the specified cache directory.
func getManifest(cacheDir string) (m Manifest, err error) {
fl, err := os.Open(filepath.Join(cacheDir, manifestFileName))
if err != nil {
return Manifest{}, err
}
defer fl.Close()
err = msgpack.NewDecoder(fl).Decode(&m)
return
}
// handleCache links the cache directory or a file within it to the destination
func handleCache(cacheDir, dest, name string, t Type) (bool, error) {
switch t {
case TypeFile:
cd, err := os.Open(cacheDir)
if err != nil {
return false, err
}
names, err := cd.Readdirnames(0)
if err == io.EOF {
break
} else if err != nil {
return false, err
}
cd.Close()
if slices.Contains(names, name) {
err = os.Link(filepath.Join(cacheDir, name), dest)
if err != nil {
return false, err
}
return true, nil
}
case TypeDir:
err := linkDir(cacheDir, dest)
if err != nil {
return false, err
}
return true, nil
}
return false, nil
}
// linkDir recursively walks through a directory, creating
// hard links for each file from the src directory to the
// dest directory. If it encounters a directory, it will
// create a directory with the same name and permissions
// in the dest directory, because hard links cannot be
// created for directories.
func linkDir(src, dest string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.Name() == manifestFileName {
return nil
}
rel, err := filepath.Rel(src, path)
if err != nil {
return err
}
newPath := filepath.Join(dest, rel)
if info.IsDir() {
return os.MkdirAll(newPath, info.Mode())
}
return os.Link(path, newPath)
})
}
func getDownloader(u string) Downloader {
for _, d := range Downloaders {
if d.MatchURL(u) {
return d
}
}
return nil
}
// normalizeURL normalizes a URL string, so that insignificant
// differences don't change the hash.
func normalizeURL(u string) (string, error) {
const normalizationFlags = purell.FlagRemoveTrailingSlash |
purell.FlagRemoveDefaultPort |
purell.FlagLowercaseHost |
purell.FlagLowercaseScheme |
purell.FlagRemoveDuplicateSlashes |
purell.FlagRemoveFragment |
purell.FlagRemoveUnnecessaryHostDots |
purell.FlagSortQuery |
purell.FlagDecodeHexHost |
purell.FlagDecodeOctalHost |
purell.FlagDecodeUnnecessaryEscapes |
purell.FlagRemoveEmptyPortSeparator
u, err := purell.NormalizeURLString(u, normalizationFlags)
if err != nil {
return "", err
}
// Fix magnet URLs after normalization
u = strings.Replace(u, "magnet://", "magnet:", 1)
return u, nil
}

264
internal/dl/file.go Normal file
View File

@ -0,0 +1,264 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package dl
import (
"bytes"
"context"
"io"
"mime"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/mholt/archiver/v4"
"github.com/schollz/progressbar/v3"
"lure.sh/lure/internal/shutils/handlers"
)
// FileDownloader downloads files using HTTP
type FileDownloader struct{}
// Name always returns "file"
func (FileDownloader) Name() string {
return "file"
}
// MatchURL always returns true, as FileDownloader
// is used as a fallback if nothing else matches
func (FileDownloader) MatchURL(string) bool {
return true
}
// Download downloads a file using HTTP. If the file is
// compressed using a supported format, it will be extracted
func (FileDownloader) Download(opts Options) (Type, string, error) {
u, err := url.Parse(opts.URL)
if err != nil {
return 0, "", err
}
query := u.Query()
name := query.Get("~name")
query.Del("~name")
archive := query.Get("~archive")
query.Del("~archive")
u.RawQuery = query.Encode()
var r io.ReadCloser
var size int64
if u.Scheme == "local" {
localFl, err := os.Open(filepath.Join(opts.LocalDir, u.Path))
if err != nil {
return 0, "", err
}
fi, err := localFl.Stat()
if err != nil {
return 0, "", err
}
size = fi.Size()
if name == "" {
name = fi.Name()
}
r = localFl
} else {
res, err := http.Get(u.String())
if err != nil {
return 0, "", err
}
size = res.ContentLength
if name == "" {
name = getFilename(res)
}
r = res.Body
}
defer r.Close()
opts.PostprocDisabled = archive == "false"
path := filepath.Join(opts.Destination, name)
fl, err := os.Create(path)
if err != nil {
return 0, "", err
}
defer fl.Close()
var bar io.WriteCloser
if opts.Progress != nil {
bar = progressbar.NewOptions64(
size,
progressbar.OptionSetDescription(name),
progressbar.OptionSetWriter(opts.Progress),
progressbar.OptionShowBytes(true),
progressbar.OptionSetWidth(10),
progressbar.OptionThrottle(65*time.Millisecond),
progressbar.OptionShowCount(),
progressbar.OptionOnCompletion(func() {
_, _ = io.WriteString(opts.Progress, "\n")
}),
progressbar.OptionSpinnerType(14),
progressbar.OptionFullWidth(),
progressbar.OptionSetRenderBlankState(true),
)
defer bar.Close()
} else {
bar = handlers.NopRWC{}
}
h, err := opts.NewHash()
if err != nil {
return 0, "", err
}
var w io.Writer
if opts.Hash != nil {
w = io.MultiWriter(fl, h, bar)
} else {
w = io.MultiWriter(fl, bar)
}
_, err = io.Copy(w, r)
if err != nil {
return 0, "", err
}
r.Close()
if opts.Hash != nil {
sum := h.Sum(nil)
if !bytes.Equal(sum, opts.Hash) {
return 0, "", ErrChecksumMismatch
}
}
if opts.PostprocDisabled {
return TypeFile, name, nil
}
_, err = fl.Seek(0, io.SeekStart)
if err != nil {
return 0, "", err
}
format, ar, err := archiver.Identify(name, fl)
if err == archiver.ErrNoMatch {
return TypeFile, name, nil
} else if err != nil {
return 0, "", err
}
err = extractFile(ar, format, name, opts)
if err != nil {
return 0, "", err
}
err = os.Remove(path)
return TypeDir, "", err
}
// extractFile extracts an archive or decompresses a file
func extractFile(r io.Reader, format archiver.Format, name string, opts Options) (err error) {
fname := format.Name()
switch format := format.(type) {
case archiver.Extractor:
err = format.Extract(context.Background(), r, nil, func(ctx context.Context, f archiver.File) error {
fr, err := f.Open()
if err != nil {
return err
}
defer fr.Close()
fi, err := f.Stat()
if err != nil {
return err
}
fm := fi.Mode()
path := filepath.Join(opts.Destination, f.NameInArchive)
err = os.MkdirAll(filepath.Dir(path), 0o755)
if err != nil {
return err
}
if f.IsDir() {
err = os.Mkdir(path, 0o755)
if err != nil {
return err
}
} else {
outFl, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fm.Perm())
if err != nil {
return err
}
defer outFl.Close()
_, err = io.Copy(outFl, fr)
return err
}
return nil
})
if err != nil {
return err
}
case archiver.Decompressor:
rc, err := format.OpenReader(r)
if err != nil {
return err
}
defer rc.Close()
path := filepath.Join(opts.Destination, name)
path = strings.TrimSuffix(path, fname)
outFl, err := os.Create(path)
if err != nil {
return err
}
_, err = io.Copy(outFl, rc)
if err != nil {
return err
}
}
return nil
}
// getFilename attempts to parse the Content-Disposition
// HTTP response header and extract a filename. If the
// header does not exist, it will use the last element
// of the path.
func getFilename(res *http.Response) (name string) {
_, params, err := mime.ParseMediaType(res.Header.Get("Content-Disposition"))
if err != nil {
return path.Base(res.Request.URL.Path)
}
if filename, ok := params["filename"]; ok {
return filename
} else {
return path.Base(res.Request.URL.Path)
}
}

198
internal/dl/git.go Normal file
View File

@ -0,0 +1,198 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package dl
import (
"errors"
"net/url"
"path"
"strconv"
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
)
// GitDownloader downloads Git repositories
type GitDownloader struct{}
// Name always returns "git"
func (GitDownloader) Name() string {
return "git"
}
// MatchURL matches any URLs that start with "git+"
func (GitDownloader) MatchURL(u string) bool {
return strings.HasPrefix(u, "git+")
}
// Download uses git to clone the repository from the specified URL.
// It allows specifying the revision, depth and recursion options
// via query string
func (GitDownloader) Download(opts Options) (Type, string, error) {
u, err := url.Parse(opts.URL)
if err != nil {
return 0, "", err
}
u.Scheme = strings.TrimPrefix(u.Scheme, "git+")
query := u.Query()
rev := query.Get("~rev")
query.Del("~rev")
name := query.Get("~name")
query.Del("~name")
depthStr := query.Get("~depth")
query.Del("~depth")
recursive := query.Get("~recursive")
query.Del("~recursive")
u.RawQuery = query.Encode()
depth := 0
if depthStr != "" {
depth, err = strconv.Atoi(depthStr)
if err != nil {
return 0, "", err
}
}
co := &git.CloneOptions{
URL: u.String(),
Depth: depth,
Progress: opts.Progress,
RecurseSubmodules: git.NoRecurseSubmodules,
}
if recursive == "true" {
co.RecurseSubmodules = git.DefaultSubmoduleRecursionDepth
}
r, err := git.PlainClone(opts.Destination, false, co)
if err != nil {
return 0, "", err
}
err = r.Fetch(&git.FetchOptions{
RefSpecs: []config.RefSpec{"+refs/*:refs/*"},
})
if err != git.NoErrAlreadyUpToDate && err != nil {
return 0, "", err
}
if rev != "" {
h, err := r.ResolveRevision(plumbing.Revision(rev))
if err != nil {
return 0, "", err
}
w, err := r.Worktree()
if err != nil {
return 0, "", err
}
err = w.Checkout(&git.CheckoutOptions{
Hash: *h,
})
if err != nil {
return 0, "", err
}
}
if name == "" {
name = strings.TrimSuffix(path.Base(u.Path), ".git")
}
return TypeDir, name, nil
}
// Update uses git to pull the repository and update it
// to the latest revision. It allows specifying the depth
// and recursion options via query string. It returns
// true if update was successful and false if the
// repository is already up-to-date
func (GitDownloader) Update(opts Options) (bool, error) {
u, err := url.Parse(opts.URL)
if err != nil {
return false, err
}
u.Scheme = strings.TrimPrefix(u.Scheme, "git+")
query := u.Query()
query.Del("~rev")
depthStr := query.Get("~depth")
query.Del("~depth")
recursive := query.Get("~recursive")
query.Del("~recursive")
u.RawQuery = query.Encode()
r, err := git.PlainOpen(opts.Destination)
if err != nil {
return false, err
}
w, err := r.Worktree()
if err != nil {
return false, err
}
depth := 0
if depthStr != "" {
depth, err = strconv.Atoi(depthStr)
if err != nil {
return false, err
}
}
po := &git.PullOptions{
Depth: depth,
Progress: opts.Progress,
RecurseSubmodules: git.NoRecurseSubmodules,
}
if recursive == "true" {
po.RecurseSubmodules = git.DefaultSubmoduleRecursionDepth
}
m, err := getManifest(opts.Destination)
manifestOK := err == nil
err = w.Pull(po)
if errors.Is(err, git.NoErrAlreadyUpToDate) {
return false, nil
} else if err != nil {
return false, err
}
if manifestOK {
err = writeManifest(opts.Destination, m)
if err != nil {
return true, err
}
}
return true, nil
}

90
internal/dl/torrent.go Normal file
View File

@ -0,0 +1,90 @@
package dl
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
)
var (
urlMatchRegex = regexp.MustCompile(`(magnet|torrent\+https?):.*`)
ErrAria2NotFound = errors.New("aria2 must be installed for torrent functionality")
ErrDestinationEmpty = errors.New("the destination directory is empty")
)
type TorrentDownloader struct{}
// Name always returns "file"
func (TorrentDownloader) Name() string {
return "torrent"
}
// MatchURL returns true if the URL is a magnet link
// or an http(s) link with a "torrent+" prefix
func (TorrentDownloader) MatchURL(u string) bool {
return urlMatchRegex.MatchString(u)
}
// Download downloads a file over the BitTorrent protocol.
func (TorrentDownloader) Download(opts Options) (Type, string, error) {
aria2Path, err := exec.LookPath("aria2c")
if err != nil {
return 0, "", ErrAria2NotFound
}
opts.URL = strings.TrimPrefix(opts.URL, "torrent+")
cmd := exec.Command(aria2Path, "--summary-interval=0", "--log-level=warn", "--seed-time=0", "--dir="+opts.Destination, opts.URL)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return 0, "", fmt.Errorf("aria2c returned an error: %w", err)
}
err = removeTorrentFiles(opts.Destination)
if err != nil {
return 0, "", err
}
return determineType(opts.Destination)
}
func removeTorrentFiles(path string) error {
filePaths, err := filepath.Glob(filepath.Join(path, "*.torrent"))
if err != nil {
return err
}
for _, filePath := range filePaths {
err = os.Remove(filePath)
if err != nil {
return err
}
}
return nil
}
func determineType(path string) (Type, string, error) {
files, err := os.ReadDir(path)
if err != nil {
return 0, "", err
}
if len(files) > 1 {
return TypeDir, "", nil
} else if len(files) == 1 {
if files[0].IsDir() {
return TypeDir, files[0].Name(), nil
} else {
return TypeFile, files[0].Name(), nil
}
}
return 0, "", ErrDestinationEmpty
}

View File

@ -0,0 +1,93 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package dlcache
import (
"context"
"crypto/sha1"
"encoding/hex"
"io"
"os"
"path/filepath"
"lure.sh/lure/internal/config"
)
// BasePath returns the base path of the download cache
func BasePath(ctx context.Context) string {
return filepath.Join(config.GetPaths(ctx).CacheDir, "dl")
}
// New creates a new directory with the given ID in the cache.
// If a directory with the same ID already exists,
// it will be deleted before creating a new one.
func New(ctx context.Context, id string) (string, error) {
h, err := hashID(id)
if err != nil {
return "", err
}
itemPath := filepath.Join(BasePath(ctx), h)
fi, err := os.Stat(itemPath)
if err == nil || (fi != nil && !fi.IsDir()) {
err = os.RemoveAll(itemPath)
if err != nil {
return "", err
}
}
err = os.MkdirAll(itemPath, 0o755)
if err != nil {
return "", err
}
return itemPath, nil
}
// Get checks if an entry with the given ID
// already exists in the cache, and if so,
// returns the directory and true. If it
// does not exist, it returns an empty string
// and false.
func Get(ctx context.Context, id string) (string, bool) {
h, err := hashID(id)
if err != nil {
return "", false
}
itemPath := filepath.Join(BasePath(ctx), h)
_, err = os.Stat(itemPath)
if err != nil {
return "", false
}
return itemPath, true
}
// hashID hashes the input ID with SHA1
// and returns the hex string of the hashed
// ID.
func hashID(id string) (string, error) {
h := sha1.New()
_, err := io.WriteString(h, id)
if err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}

View File

@ -0,0 +1,76 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package dlcache_test
import (
"context"
"crypto/sha1"
"encoding/hex"
"io"
"os"
"path/filepath"
"testing"
"lure.sh/lure/internal/config"
"lure.sh/lure/internal/dlcache"
)
func init() {
dir, err := os.MkdirTemp("/tmp", "lure-dlcache-test.*")
if err != nil {
panic(err)
}
config.GetPaths(context.Background()).RepoDir = dir
}
func TestNew(t *testing.T) {
const id = "https://example.com"
dir, err := dlcache.New(id)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
exp := filepath.Join(dlcache.BasePath(), sha1sum(id))
if dir != exp {
t.Errorf("Expected %s, got %s", exp, dir)
}
fi, err := os.Stat(dir)
if err != nil {
t.Errorf("stat: expected no error, got %s", err)
}
if !fi.IsDir() {
t.Errorf("Expected cache item to be a directory")
}
dir2, ok := dlcache.Get(id)
if !ok {
t.Errorf("Expected Get() to return valid value")
}
if dir2 != dir {
t.Errorf("Expected %s from Get(), got %s", dir, dir2)
}
}
func sha1sum(id string) string {
h := sha1.New()
_, _ = io.WriteString(h, id)
return hex.EncodeToString(h.Sum(nil))
}

110
internal/osutils/move.go Normal file
View File

@ -0,0 +1,110 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package osutils
import (
"io"
"os"
"path/filepath"
)
// Move attempts to use os.Rename and if that fails (such as for a cross-device move),
// it instead copies the source to the destination and then removes the source.
func Move(sourcePath, destPath string) error {
// Try to rename the source to the destination
err := os.Rename(sourcePath, destPath)
if err == nil {
return nil // Successful move
}
// Rename failed, so copy the source to the destination
err = copyDirOrFile(sourcePath, destPath)
if err != nil {
return err
}
// Copy successful, remove the original source
err = os.RemoveAll(sourcePath)
if err != nil {
return err
}
return nil
}
func copyDirOrFile(sourcePath, destPath string) error {
sourceInfo, err := os.Stat(sourcePath)
if err != nil {
return err
}
if sourceInfo.IsDir() {
return copyDir(sourcePath, destPath, sourceInfo)
} else if sourceInfo.Mode().IsRegular() {
return copyFile(sourcePath, destPath, sourceInfo)
} else {
// ignore non-regular files
return nil
}
}
func copyDir(sourcePath, destPath string, sourceInfo os.FileInfo) error {
err := os.MkdirAll(destPath, sourceInfo.Mode())
if err != nil {
return err
}
entries, err := os.ReadDir(sourcePath)
if err != nil {
return err
}
for _, entry := range entries {
sourceEntry := filepath.Join(sourcePath, entry.Name())
destEntry := filepath.Join(destPath, entry.Name())
err = copyDirOrFile(sourceEntry, destEntry)
if err != nil {
return err
}
}
return nil
}
func copyFile(sourcePath, destPath string, sourceInfo os.FileInfo) error {
sourceFile, err := os.Open(sourcePath)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, sourceInfo.Mode())
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,223 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package overrides
import (
"reflect"
"strings"
"lure.sh/lure/internal/cpu"
"lure.sh/lure/internal/db"
"lure.sh/lure/pkg/distro"
"golang.org/x/exp/slices"
"golang.org/x/text/language"
)
type Opts struct {
Name string
Overrides bool
LikeDistros bool
Languages []string
LanguageTags []language.Tag
}
var DefaultOpts = &Opts{
Overrides: true,
LikeDistros: true,
Languages: []string{"en"},
}
// Resolve generates a slice of possible override names in the order that they should be checked
func Resolve(info *distro.OSRelease, opts *Opts) ([]string, error) {
if opts == nil {
opts = DefaultOpts
}
if !opts.Overrides {
return []string{opts.Name}, nil
}
langs, err := parseLangs(opts.Languages, opts.LanguageTags)
if err != nil {
return nil, err
}
architectures, err := cpu.CompatibleArches(cpu.Arch())
if err != nil {
return nil, err
}
distros := []string{info.ID}
if opts.LikeDistros {
distros = append(distros, info.Like...)
}
var out []string
for _, lang := range langs {
for _, distro := range distros {
for _, arch := range architectures {
out = append(out, opts.Name+"_"+arch+"_"+distro+"_"+lang)
}
out = append(out, opts.Name+"_"+distro+"_"+lang)
}
for _, arch := range architectures {
out = append(out, opts.Name+"_"+arch+"_"+lang)
}
out = append(out, opts.Name+"_"+lang)
}
for _, distro := range distros {
for _, arch := range architectures {
out = append(out, opts.Name+"_"+arch+"_"+distro)
}
out = append(out, opts.Name+"_"+distro)
}
for _, arch := range architectures {
out = append(out, opts.Name+"_"+arch)
}
out = append(out, opts.Name)
for index, item := range out {
out[index] = strings.TrimPrefix(strings.ReplaceAll(item, "-", "_"), "_")
}
return out, nil
}
func (o *Opts) WithName(name string) *Opts {
out := &Opts{}
*out = *o
out.Name = name
return out
}
func (o *Opts) WithOverrides(v bool) *Opts {
out := &Opts{}
*out = *o
out.Overrides = v
return out
}
func (o *Opts) WithLikeDistros(v bool) *Opts {
out := &Opts{}
*out = *o
out.LikeDistros = v
return out
}
func (o *Opts) WithLanguages(langs []string) *Opts {
out := &Opts{}
*out = *o
out.Languages = langs
return out
}
func (o *Opts) WithLanguageTags(langs []string) *Opts {
out := &Opts{}
*out = *o
out.Languages = langs
return out
}
// ResolvedPackage is a LURE package after its overrides
// have been resolved
type ResolvedPackage struct {
Name string `sh:"name"`
Version string `sh:"version"`
Release int `sh:"release"`
Epoch uint `sh:"epoch"`
Description string `db:"description"`
Homepage string `db:"homepage"`
Maintainer string `db:"maintainer"`
Architectures []string `sh:"architectures"`
Licenses []string `sh:"license"`
Provides []string `sh:"provides"`
Conflicts []string `sh:"conflicts"`
Replaces []string `sh:"replaces"`
Depends []string `sh:"deps"`
BuildDepends []string `sh:"build_deps"`
OptDepends []string `sh:"opt_deps"`
}
func ResolvePackage(pkg *db.Package, overrides []string) *ResolvedPackage {
out := &ResolvedPackage{}
outVal := reflect.ValueOf(out).Elem()
pkgVal := reflect.ValueOf(pkg).Elem()
for i := 0; i < outVal.NumField(); i++ {
fieldVal := outVal.Field(i)
fieldType := fieldVal.Type()
pkgFieldVal := pkgVal.FieldByName(outVal.Type().Field(i).Name)
pkgFieldType := pkgFieldVal.Type()
if strings.HasPrefix(pkgFieldType.String(), "db.JSON") {
pkgFieldVal = pkgFieldVal.FieldByName("Val")
pkgFieldType = pkgFieldVal.Type()
}
if pkgFieldType.AssignableTo(fieldType) {
fieldVal.Set(pkgFieldVal)
continue
}
if pkgFieldVal.Kind() == reflect.Map && pkgFieldType.Elem().AssignableTo(fieldType) {
for _, override := range overrides {
overrideVal := pkgFieldVal.MapIndex(reflect.ValueOf(override))
if !overrideVal.IsValid() {
continue
}
fieldVal.Set(overrideVal)
break
}
}
}
return out
}
func parseLangs(langs []string, tags []language.Tag) ([]string, error) {
out := make([]string, len(tags)+len(langs))
for i, tag := range tags {
base, _ := tag.Base()
out[i] = base.String()
}
for i, lang := range langs {
tag, err := language.Parse(lang)
if err != nil {
return nil, err
}
base, _ := tag.Base()
out[len(tags)+i] = base.String()
}
slices.Sort(out)
out = slices.Compact(out)
return out, nil
}

View File

@ -0,0 +1,195 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package overrides_test
import (
"os"
"reflect"
"testing"
"lure.sh/lure/internal/overrides"
"lure.sh/lure/pkg/distro"
"golang.org/x/text/language"
)
var info = &distro.OSRelease{
ID: "centos",
Like: []string{"rhel", "fedora"},
}
func TestResolve(t *testing.T) {
names, err := overrides.Resolve(info, nil)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
expected := []string{
"amd64_centos_en",
"centos_en",
"amd64_rhel_en",
"rhel_en",
"amd64_fedora_en",
"fedora_en",
"amd64_en",
"en",
"amd64_centos",
"centos",
"amd64_rhel",
"rhel",
"amd64_fedora",
"fedora",
"amd64",
"",
}
if !reflect.DeepEqual(names, expected) {
t.Errorf("expected %v, got %v", expected, names)
}
}
func TestResolveName(t *testing.T) {
names, err := overrides.Resolve(info, &overrides.Opts{
Name: "deps",
Overrides: true,
LikeDistros: true,
})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
expected := []string{
"deps_amd64_centos",
"deps_centos",
"deps_amd64_rhel",
"deps_rhel",
"deps_amd64_fedora",
"deps_fedora",
"deps_amd64",
"deps",
}
if !reflect.DeepEqual(names, expected) {
t.Errorf("expected %v, got %v", expected, names)
}
}
func TestResolveArch(t *testing.T) {
os.Setenv("LURE_ARCH", "arm7")
defer os.Setenv("LURE_ARCH", "")
names, err := overrides.Resolve(info, &overrides.Opts{
Name: "deps",
Overrides: true,
LikeDistros: true,
})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
expected := []string{
"deps_arm7_centos",
"deps_arm6_centos",
"deps_arm5_centos",
"deps_centos",
"deps_arm7_rhel",
"deps_arm6_rhel",
"deps_arm5_rhel",
"deps_rhel",
"deps_arm7_fedora",
"deps_arm6_fedora",
"deps_arm5_fedora",
"deps_fedora",
"deps_arm7",
"deps_arm6",
"deps_arm5",
"deps",
}
if !reflect.DeepEqual(names, expected) {
t.Errorf("expected %v, got %v", expected, names)
}
}
func TestResolveNoLikeDistros(t *testing.T) {
names, err := overrides.Resolve(info, &overrides.Opts{
Overrides: true,
LikeDistros: false,
})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
expected := []string{
"amd64_centos",
"centos",
"amd64",
"",
}
if !reflect.DeepEqual(names, expected) {
t.Errorf("expected %v, got %v", expected, names)
}
}
func TestResolveNoOverrides(t *testing.T) {
names, err := overrides.Resolve(info, &overrides.Opts{
Name: "deps",
Overrides: false,
LikeDistros: false,
})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
expected := []string{"deps"}
if !reflect.DeepEqual(names, expected) {
t.Errorf("expected %v, got %v", expected, names)
}
}
func TestResolveLangs(t *testing.T) {
names, err := overrides.Resolve(info, &overrides.Opts{
Overrides: true,
Languages: []string{"ru_RU", "en", "en_US"},
LanguageTags: []language.Tag{language.BritishEnglish},
})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
expected := []string{
"amd64_centos_en",
"centos_en",
"amd64_en",
"en",
"amd64_centos_ru",
"centos_ru",
"amd64_ru",
"ru",
"amd64_centos",
"centos",
"amd64",
"",
}
if !reflect.DeepEqual(names, expected) {
t.Errorf("expected %v, got %v", expected, names)
}
}

View File

@ -0,0 +1,37 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package pager
import (
"bytes"
"io"
"github.com/alecthomas/chroma/v2/quick"
)
func SyntaxHighlightBash(r io.Reader, style string) (string, error) {
data, err := io.ReadAll(r)
if err != nil {
return "", err
}
w := &bytes.Buffer{}
err = quick.Highlight(w, string(data), "bash", "terminal", style)
return w.String(), err
}

143
internal/pager/pager.go Normal file
View File

@ -0,0 +1,143 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package pager
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/wordwrap"
)
var (
titleStyle lipgloss.Style
infoStyle lipgloss.Style
)
func init() {
b1 := lipgloss.RoundedBorder()
b1.Right = "\u251C"
titleStyle = lipgloss.NewStyle().BorderStyle(b1).Padding(0, 1)
b2 := lipgloss.RoundedBorder()
b2.Left = "\u2524"
infoStyle = titleStyle.Copy().BorderStyle(b2)
}
type Pager struct {
model pagerModel
}
func New(name, content string) *Pager {
return &Pager{
model: pagerModel{
name: name,
content: content,
},
}
}
func (p *Pager) Run() error {
prog := tea.NewProgram(
p.model,
tea.WithMouseCellMotion(),
tea.WithAltScreen(),
)
_, err := prog.Run()
return err
}
type pagerModel struct {
name string
content string
ready bool
viewport viewport.Model
}
func (pm pagerModel) Init() tea.Cmd {
return nil
}
func (pm pagerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
switch msg := msg.(type) {
case tea.KeyMsg:
k := msg.String()
if k == "ctrl+c" || k == "q" || k == "esc" {
return pm, tea.Quit
}
case tea.WindowSizeMsg:
headerHeight := lipgloss.Height(pm.headerView())
footerHeight := lipgloss.Height(pm.footerView())
verticalMarginHeight := headerHeight + footerHeight
if !pm.ready {
pm.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
pm.viewport.HighPerformanceRendering = true
pm.viewport.YPosition = headerHeight + 1
pm.viewport.SetContent(wordwrap.String(pm.content, msg.Width))
pm.ready = true
} else {
pm.viewport.Width = msg.Width
pm.viewport.Height = msg.Height - verticalMarginHeight
}
cmds = append(cmds, viewport.Sync(pm.viewport))
}
// Handle keyboard and mouse events in the viewport
pm.viewport, cmd = pm.viewport.Update(msg)
cmds = append(cmds, cmd)
return pm, tea.Batch(cmds...)
}
func (pm pagerModel) View() string {
if !pm.ready {
return "\n Initializing..."
}
return fmt.Sprintf("%s\n%s\n%s", pm.headerView(), pm.viewport.View(), pm.footerView())
}
func (pm pagerModel) headerView() string {
title := titleStyle.Render(pm.name)
line := strings.Repeat("─", max(0, pm.viewport.Width-lipgloss.Width(title)))
return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
}
func (pm pagerModel) footerView() string {
info := infoStyle.Render(fmt.Sprintf("%3.f%%", pm.viewport.ScrollPercent()*100))
line := strings.Repeat("─", max(0, pm.viewport.Width-lipgloss.Width(info)))
return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}

View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -21,21 +21,19 @@ package decoder
import (
"context"
"errors"
"fmt"
"reflect"
"runtime"
"strings"
"github.com/mitchellh/mapstructure"
"go.arsenm.dev/lure/distro"
"go.arsenm.dev/lure/internal/cpu"
"lure.sh/lure/internal/overrides"
"lure.sh/lure/pkg/distro"
"golang.org/x/exp/slices"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
)
var ErrInvalidType = errors.New("val must be a pointer to a struct")
var ErrNotPointerToStruct = errors.New("val must be a pointer to a struct")
type VarNotFoundError struct {
name string
@ -45,19 +43,29 @@ func (nfe VarNotFoundError) Error() string {
return "required variable '" + nfe.name + "' could not be found"
}
type InvalidTypeError struct {
name string
vartype string
exptype string
}
func (ite InvalidTypeError) Error() string {
return "variable '" + ite.name + "' is of type " + ite.vartype + ", but " + ite.exptype + " is expected"
}
// Decoder provides methods for decoding variable values
type Decoder struct {
info *distro.OSRelease
runner *interp.Runner
Runner *interp.Runner
// Enable distro overrides (true by default)
Overrides bool
// Enable using like distros for overrides (true by default)
// Enable using like distros for overrides
LikeDistros bool
}
// New creates a new variable decoder
func New(info *distro.OSRelease, runner *interp.Runner) *Decoder {
return &Decoder{info, runner, true, true}
return &Decoder{info, runner, true, len(info.Like) > 0}
}
// DecodeVar decodes a variable to val using reflection.
@ -70,8 +78,20 @@ func (d *Decoder) DecodeVar(name string, val any) error {
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
WeaklyTypedInput: true,
Result: val,
TagName: "sh",
DecodeHook: mapstructure.DecodeHookFuncValue(func(from, to reflect.Value) (interface{}, error) {
if strings.Contains(to.Type().String(), "db.JSON") {
valType := to.FieldByName("Val").Type()
if !from.Type().AssignableTo(valType) {
return nil, InvalidTypeError{name, from.Type().String(), valType.String()}
}
to.FieldByName("Val").Set(from)
return to, nil
}
return from.Interface(), nil
}),
Result: val,
TagName: "sh",
})
if err != nil {
return err
@ -92,11 +112,11 @@ func (d *Decoder) DecodeVar(name string, val any) error {
func (d *Decoder) DecodeVars(val any) error {
valKind := reflect.TypeOf(val).Kind()
if valKind != reflect.Pointer {
return ErrInvalidType
return ErrNotPointerToStruct
} else {
elemKind := reflect.TypeOf(val).Elem().Kind()
if elemKind != reflect.Struct {
return ErrInvalidType
return ErrNotPointerToStruct
}
}
@ -153,7 +173,7 @@ func (d *Decoder) GetFunc(name string) (ScriptFunc, bool) {
}
return func(ctx context.Context, opts ...interp.RunnerOption) error {
sub := d.runner.Subshell()
sub := d.Runner.Subshell()
for _, opt := range opts {
opt(sub)
}
@ -162,9 +182,13 @@ func (d *Decoder) GetFunc(name string) (ScriptFunc, bool) {
}
func (d *Decoder) getFunc(name string) *syntax.Stmt {
names := d.genPossibleNames(name)
names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name))
if err != nil {
return nil
}
for _, fnName := range names {
fn, ok := d.runner.Funcs[fnName]
fn, ok := d.Runner.Funcs[fnName]
if ok {
return fn
}
@ -175,13 +199,17 @@ func (d *Decoder) getFunc(name string) *syntax.Stmt {
// getVar gets a variable based on its name, taking into account
// override variables and nameref variables.
func (d *Decoder) getVar(name string) *expand.Variable {
names := d.genPossibleNames(name)
names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name))
if err != nil {
return nil
}
for _, varName := range names {
val, ok := d.runner.Vars[varName]
val, ok := d.Runner.Vars[varName]
if ok {
// Resolve nameref variables
_, resolved := val.Resolve(expand.FuncEnviron(func(s string) string {
if val, ok := d.runner.Vars[s]; ok {
if val, ok := d.Runner.Vars[s]; ok {
return val.String()
}
return ""
@ -193,43 +221,3 @@ func (d *Decoder) getVar(name string) *expand.Variable {
}
return nil
}
// genPossibleNames generates a slice of the possible names that
// could be used in the order that they should be checked
func (d *Decoder) genPossibleNames(name string) []string {
if !d.Overrides {
return []string{name}
}
architectures := []string{runtime.GOARCH}
if runtime.GOARCH == "arm" {
// More specific goes first
architectures[0] = cpu.ARMVariant()
architectures = append(architectures, "arm")
}
distros := []string{d.info.ID}
if d.LikeDistros {
distros = append(distros, d.info.Like...)
}
var out []string
for _, arch := range architectures {
for _, distro := range distros {
out = append(
out,
fmt.Sprintf("%s_%s_%s", name, arch, distro),
fmt.Sprintf("%s_%s", name, distro),
)
}
out = append(out, fmt.Sprintf("%s_%s", name, arch))
}
out = append(out, name)
for index, item := range out {
out[index] = strings.ReplaceAll(item, "-", "_")
}
return out
}

View File

@ -0,0 +1,224 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package decoder_test
import (
"bytes"
"context"
"errors"
"os"
"reflect"
"strings"
"testing"
"lure.sh/lure/internal/shutils/decoder"
"lure.sh/lure/pkg/distro"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
)
type BuildVars struct {
Name string `sh:"name,required"`
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"`
Replaces []string `sh:"replaces"`
}
const testScript = `
name='test'
version='0.0.1'
release=1
epoch=2
desc="Test package"
homepage='https://lure.arsenm.dev'
maintainer='Arsen Musayelyan <arsen@arsenm.dev>'
architectures=('arm64' 'amd64')
license=('GPL-3.0-or-later')
provides=('test')
conflicts=('test')
replaces=('test-old')
replaces_test_os=('test-legacy')
deps=('sudo')
build_deps=('golang')
build_deps_arch=('go')
test() {
echo "Test"
}
package() {
install-binary test
}
`
var osRelease = &distro.OSRelease{
ID: "test_os",
Like: []string{"arch"},
}
func TestDecodeVars(t *testing.T) {
ctx := context.Background()
fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
runner, err := interp.New()
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
err = runner.Run(ctx, fl)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
dec := decoder.New(osRelease, runner)
var bv BuildVars
err = dec.DecodeVars(&bv)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
expected := BuildVars{
Name: "test",
Version: "0.0.1",
Release: 1,
Epoch: 2,
Description: "Test package",
Homepage: "https://lure.arsenm.dev",
Maintainer: "Arsen Musayelyan <arsen@arsenm.dev>",
Architectures: []string{"arm64", "amd64"},
Licenses: []string{"GPL-3.0-or-later"},
Provides: []string{"test"},
Conflicts: []string{"test"},
Replaces: []string{"test-legacy"},
Depends: []string{"sudo"},
BuildDepends: []string{"go"},
}
if !reflect.DeepEqual(bv, expected) {
t.Errorf("Expected %v, got %v", expected, bv)
}
}
func TestDecodeVarsMissing(t *testing.T) {
ctx := context.Background()
const testScript = `
name='test'
epoch=2
desc="Test package"
homepage='https://lure.arsenm.dev'
maintainer='Arsen Musayelyan <arsen@arsenm.dev>'
architectures=('arm64' 'amd64')
license=('GPL-3.0-or-later')
provides=('test')
conflicts=('test')
replaces=('test-old')
replaces_test_os=('test-legacy')
deps=('sudo')
build_deps=('golang')
build_deps_arch=('go')
test() {
echo "Test"
}
package() {
install-binary test
}
`
fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
runner, err := interp.New()
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
err = runner.Run(ctx, fl)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
dec := decoder.New(osRelease, runner)
var bv BuildVars
err = dec.DecodeVars(&bv)
var notFoundErr decoder.VarNotFoundError
if !errors.As(err, &notFoundErr) {
t.Fatalf("Expected VarNotFoundError, got %T %v", err, err)
}
}
func TestGetFunc(t *testing.T) {
ctx := context.Background()
fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
runner, err := interp.New()
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
err = runner.Run(ctx, fl)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
dec := decoder.New(osRelease, runner)
fn, ok := dec.GetFunc("test")
if !ok {
t.Fatalf("Expected test() function to exist")
}
buf := &bytes.Buffer{}
err = fn(ctx, interp.StdIO(os.Stdin, buf, buf))
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
if buf.String() != "Test\n" {
t.Fatalf(`Expected "Test\n", got %#v`, buf.String())
}
}

View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package shutils
package handlers
import (
"context"
@ -39,18 +39,25 @@ type ExecFunc func(hc interp.HandlerContext, name string, args []string) error
type ExecFuncs map[string]ExecFunc
func (ef ExecFuncs) ExecHandler(ctx context.Context, args []string) error {
name := args[0]
// ExecHandler returns a new ExecHandlerFunc that falls back to fallback
// if the command cannot be found in the map. If fallback is nil, the default
// handler is used.
func (ef ExecFuncs) ExecHandler(fallback interp.ExecHandlerFunc) interp.ExecHandlerFunc {
return func(ctx context.Context, args []string) error {
name := args[0]
if fn, ok := ef[name]; ok {
hctx := interp.HandlerCtx(ctx)
if len(args) > 1 {
return fn(hctx, args[0], args[1:])
} else {
return fn(hctx, args[0], nil)
if fn, ok := ef[name]; ok {
hctx := interp.HandlerCtx(ctx)
if len(args) > 1 {
return fn(hctx, args[0], args[1:])
} else {
return fn(hctx, args[0], nil)
}
}
}
defExec := interp.DefaultExecHandler(2 * time.Second)
return defExec(ctx, args)
if fallback == nil {
fallback = interp.DefaultExecHandler(2 * time.Second)
}
return fallback(ctx, args)
}
}

View File

@ -0,0 +1,124 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package handlers_test
import (
"context"
"strings"
"testing"
"lure.sh/lure/internal/shutils/handlers"
"lure.sh/lure/internal/shutils/decoder"
"lure.sh/lure/pkg/distro"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
)
const testScript = `
name='test'
version='0.0.1'
release=1
epoch=2
desc="Test package"
homepage='https://lure.sh'
maintainer='Elara Musayelyan <elara@elara.ws>'
architectures=('arm64' 'amd64')
license=('GPL-3.0-or-later')
provides=('test')
conflicts=('test')
replaces=('test-old')
replaces_test_os=('test-legacy')
deps=('sudo')
build_deps=('golang')
build_deps_arch=('go')
test() {
test-cmd "Hello, World"
test-fb
}
package() {
install-binary test
}
`
var osRelease = &distro.OSRelease{
ID: "test_os",
Like: []string{"arch"},
}
func TestExecFuncs(t *testing.T) {
ctx := context.Background()
fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
runner, err := interp.New()
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
err = runner.Run(ctx, fl)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
dec := decoder.New(osRelease, runner)
fn, ok := dec.GetFunc("test")
if !ok {
t.Fatalf("Expected test() function to exist")
}
eh := shutils.ExecFuncs{
"test-cmd": func(hc interp.HandlerContext, name string, args []string) error {
if name != "test-cmd" {
t.Errorf("Expected name to be 'test-cmd', got '%s'", name)
}
if len(args) < 1 {
t.Fatalf("Expected at least one argument, got %d", len(args))
}
if args[0] != "Hello, World" {
t.Errorf("Expected first argument to be 'Hello, World', got '%s'", args[0])
}
return nil
},
}
fbInvoked := false
fbHandler := func(context.Context, []string) error {
fbInvoked = true
return nil
}
err = fn(ctx, interp.ExecHandler(eh.ExecHandler(fbHandler)))
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
if !fbInvoked {
t.Errorf("Expected fallback handler to be invoked")
}
}

View File

@ -0,0 +1,114 @@
package handlers
import (
"context"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"syscall"
"time"
"lure.sh/fakeroot"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
)
// FakerootExecHandler was extracted from github.com/mvdan/sh/interp/handler.go
// and modified to run commands in a fakeroot environent.
func FakerootExecHandler(killTimeout time.Duration) interp.ExecHandlerFunc {
return func(ctx context.Context, args []string) error {
hc := interp.HandlerCtx(ctx)
path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0])
if err != nil {
fmt.Fprintln(hc.Stderr, err)
return interp.NewExitStatus(127)
}
cmd := &exec.Cmd{
Path: path,
Args: args,
Env: execEnv(hc.Env),
Dir: hc.Dir,
Stdin: hc.Stdin,
Stdout: hc.Stdout,
Stderr: hc.Stderr,
}
err = fakeroot.Apply(cmd)
if err != nil {
return err
}
err = cmd.Start()
if err == nil {
if done := ctx.Done(); done != nil {
go func() {
<-done
if killTimeout <= 0 || runtime.GOOS == "windows" {
_ = cmd.Process.Signal(os.Kill)
return
}
// TODO: don't temporarily leak this goroutine
// if the program stops itself with the
// interrupt.
go func() {
time.Sleep(killTimeout)
_ = cmd.Process.Signal(os.Kill)
}()
_ = cmd.Process.Signal(os.Interrupt)
}()
}
err = cmd.Wait()
}
switch x := err.(type) {
case *exec.ExitError:
// started, but errored - default to 1 if OS
// doesn't have exit statuses
if status, ok := x.Sys().(syscall.WaitStatus); ok {
if status.Signaled() {
if ctx.Err() != nil {
return ctx.Err()
}
return interp.NewExitStatus(uint8(128 + status.Signal()))
}
return interp.NewExitStatus(uint8(status.ExitStatus()))
}
return interp.NewExitStatus(1)
case *exec.Error:
// did not start
fmt.Fprintf(hc.Stderr, "%v\n", err)
return interp.NewExitStatus(127)
default:
return err
}
}
}
// execEnv was extracted from github.com/mvdan/sh/interp/vars.go
func execEnv(env expand.Environ) []string {
list := make([]string, 0, 64)
env.Each(func(name string, vr expand.Variable) bool {
if !vr.IsSet() {
// If a variable is set globally but unset in the
// runner, we need to ensure it's not part of the final
// list. Seems like zeroing the element is enough.
// This is a linear search, but this scenario should be
// rare, and the number of variables shouldn't be large.
for i, kv := range list {
if strings.HasPrefix(kv, name+"=") {
list[i] = ""
}
}
}
if vr.Exported && vr.Kind == expand.String {
list = append(list, name+"="+vr.String())
}
return true
})
return list
}

View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package shutils
package handlers
import (
"context"
@ -46,8 +46,8 @@ func (NopRWC) Read([]byte) (int, error) {
return 0, io.EOF
}
func (NopRWC) Write([]byte) (int, error) {
return 0, io.EOF
func (NopRWC) Write(b []byte) (int, error) {
return len(b), nil
}
func (NopRWC) Close() error {

View File

@ -0,0 +1,58 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package handlers_test
import (
"bytes"
"context"
"os"
"strings"
"testing"
"lure.sh/lure/internal/shutils/handlers"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
)
func TestNopExec(t *testing.T) {
ctx := context.Background()
fl, err := syntax.NewParser().Parse(strings.NewReader(`/bin/echo test`), "lure.sh")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
buf := &bytes.Buffer{}
runner, err := interp.New(
interp.ExecHandler(handlers.NopExec),
interp.StdIO(os.Stdin, buf, buf),
)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
err = runner.Run(ctx, fl)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
if buf.String() != "" {
t.Fatalf("Expected empty string, got %#v", buf.String())
}
}

View File

@ -0,0 +1,80 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package handlers
import (
"context"
"io"
"io/fs"
"path/filepath"
"strings"
"time"
"golang.org/x/exp/slices"
"mvdan.cc/sh/v3/interp"
)
func RestrictedReadDir(allowedPrefixes ...string) interp.ReadDirHandlerFunc {
return func(ctx context.Context, s string) ([]fs.FileInfo, error) {
path := filepath.Clean(s)
for _, allowedPrefix := range allowedPrefixes {
if strings.HasPrefix(path, allowedPrefix) {
return interp.DefaultReadDirHandler()(ctx, s)
}
}
return nil, fs.ErrNotExist
}
}
func RestrictedStat(allowedPrefixes ...string) interp.StatHandlerFunc {
return func(ctx context.Context, s string, b bool) (fs.FileInfo, error) {
path := filepath.Clean(s)
for _, allowedPrefix := range allowedPrefixes {
if strings.HasPrefix(path, allowedPrefix) {
return interp.DefaultStatHandler()(ctx, s, b)
}
}
return nil, fs.ErrNotExist
}
}
func RestrictedOpen(allowedPrefixes ...string) interp.OpenHandlerFunc {
return func(ctx context.Context, s string, i int, fm fs.FileMode) (io.ReadWriteCloser, error) {
path := filepath.Clean(s)
for _, allowedPrefix := range allowedPrefixes {
if strings.HasPrefix(path, allowedPrefix) {
return interp.DefaultOpenHandler()(ctx, s, i, fm)
}
}
return NopRWC{}, nil
}
}
func RestrictedExec(allowedCmds ...string) interp.ExecHandlerFunc {
return func(ctx context.Context, args []string) error {
if slices.Contains(allowedCmds, args[0]) {
return interp.DefaultExecHandler(2*time.Second)(ctx, args)
}
return nil
}
}

View File

@ -1,4 +1,22 @@
package main
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package helpers
import (
"errors"
@ -10,8 +28,10 @@ import (
"strings"
"unsafe"
"go.arsenm.dev/lure/internal/shutils"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"golang.org/x/exp/slices"
"lure.sh/lure/internal/shutils/handlers"
"mvdan.cc/sh/v3/interp"
)
@ -20,25 +40,34 @@ var (
ErrNoDetectManNum = errors.New("manual number cannot be detected from the filename")
)
var helpers = shutils.ExecFuncs{
// Helpers contains all the helper commands
var Helpers = handlers.ExecFuncs{
"install-binary": installHelperCmd("/usr/bin", 0o755),
"install-systemd-user": installHelperCmd("/usr/lib/systemd/user", 0o644),
"install-systemd": installHelperCmd("/usr/lib/systemd/system", 0o644),
"install-config": installHelperCmd("/etc", 0o644),
"install-license": installHelperCmd("/usr/share/licenses", 0o644),
"install-desktop": installHelperCmd("/usr/share/applications", 0o644),
"install-icon": installHelperCmd("/usr/share/pixmaps", 0o644),
"install-manual": installManualCmd,
"install-completion": installCompletionCmd,
"install-library": installLibraryCmd,
"git-version": gitVersionCmd,
}
func installHelperCmd(prefix string, perms os.FileMode) shutils.ExecFunc {
// Restricted contains restricted read-only helper commands
// that don't modify any state
var Restricted = handlers.ExecFuncs{
"git-version": gitVersionCmd,
}
func installHelperCmd(prefix string, perms os.FileMode) handlers.ExecFunc {
return func(hc interp.HandlerContext, cmd string, args []string) error {
if len(args) < 1 {
return shutils.InsufficientArgsError(cmd, 1, len(args))
return handlers.InsufficientArgsError(cmd, 1, len(args))
}
from := args[0]
from := resolvePath(hc, args[0])
to := ""
if len(args) > 1 {
to = filepath.Join(hc.Env.Get("pkgdir").Str, prefix, args[1])
@ -56,10 +85,10 @@ func installHelperCmd(prefix string, perms os.FileMode) shutils.ExecFunc {
func installManualCmd(hc interp.HandlerContext, cmd string, args []string) error {
if len(args) < 1 {
return shutils.InsufficientArgsError(cmd, 1, len(args))
return handlers.InsufficientArgsError(cmd, 1, len(args))
}
from := args[0]
from := resolvePath(hc, args[0])
number := filepath.Base(from)
// The man page may be compressed with gzip.
// If it is, the .gz extension must be removed to properly
@ -86,7 +115,7 @@ func installCompletionCmd(hc interp.HandlerContext, cmd string, args []string) e
}
if len(args) < 2 {
return shutils.InsufficientArgsError(cmd, 2, len(args))
return handlers.InsufficientArgsError(cmd, 2, len(args))
}
shell := args[0]
@ -95,7 +124,7 @@ func installCompletionCmd(hc interp.HandlerContext, cmd string, args []string) e
var prefix string
switch shell {
case "bash":
prefix = "/usr/share/bash-completion/completion"
prefix = "/usr/share/bash-completion/completions"
case "zsh":
prefix = "/usr/share/zsh/site-functions"
name = "_" + name
@ -179,16 +208,52 @@ func getLibPrefix(hc interp.HandlerContext) string {
architecture := hc.Env.Get("ARCH").Str
if distroID == "debian" || slices.Contains(distroLike, "debian") {
triple, ok := multiarchTupleMap[architecture]
if distroID == "debian" || slices.Contains(distroLike, "debian") ||
distroID == "ubuntu" || slices.Contains(distroLike, "ubuntu") {
tuple, ok := multiarchTupleMap[architecture]
if ok {
out = filepath.Join("/usr/lib", triple)
out = filepath.Join("/usr/lib", tuple)
}
}
return out
}
func gitVersionCmd(hc interp.HandlerContext, cmd string, args []string) error {
path := hc.Dir
if len(args) > 0 {
path = resolvePath(hc, args[0])
}
r, err := git.PlainOpen(path)
if err != nil {
return fmt.Errorf("git-version: %w", err)
}
revNum := 0
commits, err := r.Log(&git.LogOptions{})
if err != nil {
return fmt.Errorf("git-version: %w", err)
}
commits.ForEach(func(*object.Commit) error {
revNum++
return nil
})
HEAD, err := r.Head()
if err != nil {
return fmt.Errorf("git-version: %w", err)
}
hash := HEAD.Hash().String()
fmt.Fprintf(hc.Stdout, "%d.%s\n", revNum, hash[:7])
return nil
}
func helperInstall(from, to string, perms os.FileMode) error {
err := os.MkdirAll(filepath.Dir(to), 0o755)
if err != nil {
@ -210,3 +275,10 @@ func helperInstall(from, to string, perms os.FileMode) error {
_, err = io.Copy(dst, src)
return err
}
func resolvePath(hc interp.HandlerContext, path string) string {
if !filepath.IsAbs(path) {
return filepath.Join(hc.Dir, path)
}
return path
}

View File

@ -0,0 +1,155 @@
[[translation]]
id = 1228660974
value = 'Pulling repository'
[[translation]]
id = 2779805870
value = 'Repository up to date'
[[translation]]
id = 1433222829
value = 'Would you like to view the build script for'
[[translation]]
id = 2470847050
value = 'Failed to prompt user to view build script'
[[translation]]
id = 855659503
value = 'Would you still like to continue?'
[[translation]]
id = 1997041569
value = 'User chose not to continue after reading script'
[[translation]]
id = 2347700990
value = 'Building package'
[[translation]]
id = 2105058868
value = 'Downloading sources'
[[translation]]
id = 1884485082
value = 'Downloading source'
[[translation]]
id = 1519177982
value = 'Error building package'
[[translation]]
id = 2125220917
value = 'Choose which package(s) to install'
[[translation]]
id = 812531604
value = 'Error prompting for choice of package'
[[translation]]
id = 1040982801
value = 'Updating version'
[[translation]]
id = 1014897988
value = 'Remove build dependencies?'
[[translation]]
id = 2205430948
value = 'Installing build dependencies'
[[translation]]
id = 2522710805
value = 'Installing dependencies'
[[translation]]
id = 3602138206
value = 'Error installing package'
[[translation]]
id = 2235794125
value = 'Would you like to remove build dependencies?'
[[translation]]
id = 2562049386
value = "Your system's CPU architecture doesn't match this package. Do you want to build anyway?"
[[translation]]
id = 4006393493
value = 'The checksums array must be the same length as sources'
[[translation]]
id = 3759891273
value = 'The package() function is required'
[[translation]]
id = 1057080231
value = 'Executing package()'
[[translation]]
id = 2687735200
value = 'Executing prepare()'
[[translation]]
id = 535572372
value = 'Executing version()'
[[translation]]
id = 436644691
value = 'Executing build()'
[[translation]]
id = 1393316459
value = 'This package is already installed'
[[translation]]
id = 1267660189
value = 'Source can be updated, updating if required'
[[translation]]
id = 21753247
value = 'Source found in cache, linked to destination'
[[translation]]
id = 257354570
value = 'Compressing package'
[[translation]]
id = 2952487371
value = 'Building package metadata'
[[translation]]
id = 3121791194
value = 'Running LURE as root is forbidden as it may cause catastrophic damage to your system'
[[translation]]
id = 1256604213
value = 'Waiting for torrent metadata'
[[translation]]
id = 432261354
value = 'Downloading torrent file'
[[translation]]
id = 1579384326
value = 'name'
[[translation]]
id = 3206337475
value = 'version'
[[translation]]
id = 1810056261
value = 'new'
[[translation]]
id = 1602912115
value = 'source'
[[translation]]
id = 2363381545
value = 'type'
[[translation]]
id = 3419504365
value = 'downloader'

View File

@ -0,0 +1,151 @@
[[translation]]
id = 1228660974
value = 'Скачивание репозитория'
[[translation]]
id = 2779805870
value = 'Репозиторий уже обновлен'
[[translation]]
id = 1433222829
value = 'Показать скрипт для пакета'
[[translation]]
id = 2470847050
value = 'Не удалось предложить просмотреть скрипт'
[[translation]]
id = 855659503
value = 'Продолжить?'
[[translation]]
id = 1997041569
value = 'Пользователь решил не продолжать после просмотра скрипта'
[[translation]]
id = 2347700990
value = 'Сборка пакета'
[[translation]]
id = 2105058868
value = 'Скачивание файлов'
[[translation]]
id = 1884485082
value = 'Скачивание источника'
[[translation]]
id = 1519177982
value = 'Ошибка при сборке пакета'
[[translation]]
id = 2125220917
value = 'Выберите, какие пакеты установить'
[[translation]]
id = 812531604
value = 'Ошибка при запросе выбора пакета'
[[translation]]
id = 1040982801
value = 'Обновление версии'
[[translation]]
id = 2235794125
value = 'Удалить зависимости сборки?'
[[translation]]
id = 2205430948
value = 'Установка зависимостей сборки'
[[translation]]
id = 2522710805
value = 'Установка зависимостей'
[[translation]]
id = 3602138206
value = 'Ошибка при установке пакета'
[[translation]]
id = 1057080231
value = 'Вызов функции package()'
[[translation]]
id = 2687735200
value = 'Вызов функции prepare()'
[[translation]]
id = 535572372
value = 'Вызов функции version()'
[[translation]]
id = 436644691
value = 'Вызов функции build()'
[[translation]]
id = 2562049386
value = "Архитектура процессора вашей системы не соответствует этому пакету. Продолжать несмотря на это?"
[[translation]]
id = 3759891273
value = 'Функция package() необходима'
[[translation]]
id = 4006393493
value = 'Массив checksums должен быть той же длины, что и sources'
[[translation]]
id = 1393316459
value = 'Этот пакет уже установлен'
[[translation]]
id = 1267660189
value = 'Источник может быть обновлен, если требуется, обновляем'
[[translation]]
id = 21753247
value = 'Источник найден в кэше'
[[translation]]
id = 257354570
value = 'Сжатие пакета'
[[translation]]
id = 2952487371
value = 'Создание метаданных пакета'
[[translation]]
id = 3121791194
value = 'Запуск LURE от имени root запрещен, так как это может привести к катастрофическому повреждению вашей системы'
[[translation]]
id = 1256604213
value = 'Ожидание метаданных торрента'
[[translation]]
id = 432261354
value = 'Скачивание торрент-файла'
[[translation]]
id = 1579384326
value = 'название'
[[translation]]
id = 3206337475
value = 'версия'
[[translation]]
id = 1810056261
value = 'новая'
[[translation]]
id = 1602912115
value = 'источник'
[[translation]]
id = 2363381545
value = 'вид'
[[translation]]
id = 3419504365
value = 'протокол-скачивание'

View File

@ -0,0 +1,56 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package translations
import (
"context"
"embed"
"sync"
"go.elara.ws/logger"
"lure.sh/lure/pkg/loggerctx"
"go.elara.ws/translate"
"golang.org/x/text/language"
)
//go:embed files
var translationFS embed.FS
var (
mu sync.Mutex
translator *translate.Translator
)
func Translator(ctx context.Context) *translate.Translator {
mu.Lock()
defer mu.Unlock()
log := loggerctx.From(ctx)
if translator == nil {
t, err := translate.NewFromFS(translationFS)
if err != nil {
log.Fatal("Error creating new translator").Err(err).Send()
}
translator = &t
}
return translator
}
func NewLogger(ctx context.Context, l logger.Logger, lang language.Tag) *translate.TranslatedLogger {
return translate.NewLogger(l, *Translator(ctx), lang)
}

70
internal/types/build.go Normal file
View File

@ -0,0 +1,70 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package types
import "lure.sh/lure/pkg/manager"
type BuildOpts struct {
Script string
Manager manager.Manager
Clean bool
Interactive bool
}
// BuildVars represents the script variables required
// to build a package
type BuildVars struct {
Name string `sh:"name,required"`
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"`
}
type Scripts struct {
PreInstall string `sh:"preinstall"`
PostInstall string `sh:"postinstall"`
PreRemove string `sh:"preremove"`
PostRemove string `sh:"postremove"`
PreUpgrade string `sh:"preupgrade"`
PostUpgrade string `sh:"postupgrade"`
PreTrans string `sh:"pretrans"`
PostTrans string `sh:"posttrans"`
}
type Directories struct {
BaseDir string
SrcDir string
PkgDir string
ScriptDir string
}

38
internal/types/config.go Normal file
View File

@ -0,0 +1,38 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package types
// Config represents the LURE configuration file
type Config struct {
RootCmd string `toml:"rootCmd"`
PagerStyle string `toml:"pagerStyle"`
IgnorePkgUpdates []string `toml:"ignorePkgUpdates"`
Repos []Repo `toml:"repo"`
Unsafe Unsafe `toml:"unsafe"`
}
// Repo represents a LURE repo within a configuration file
type Repo struct {
Name string `toml:"name"`
URL string `toml:"url"`
}
type Unsafe struct {
AllowRunAsRoot bool `toml:"allowRunAsRoot"`
}

26
internal/types/repo.go Normal file
View File

@ -0,0 +1,26 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package types
// RepoConfig represents a LURE repo's lure-repo.toml file.
type RepoConfig struct {
Repo struct {
MinVersion string `toml:"minVersion"`
}
}

95
list.go
View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -22,28 +22,87 @@ import (
"fmt"
"github.com/urfave/cli/v2"
"go.arsenm.dev/lure/distro"
"lure.sh/lure/internal/config"
"lure.sh/lure/internal/db"
"lure.sh/lure/pkg/loggerctx"
"lure.sh/lure/pkg/manager"
"lure.sh/lure/pkg/repos"
"golang.org/x/exp/slices"
)
func listCmd(c *cli.Context) error {
info, err := distro.ParseOSRelease(c.Context)
if err != nil {
log.Fatal("Error parsing os-release").Err(err).Send()
}
var listCmd = &cli.Command{
Name: "list",
Usage: "List LURE repo packages",
Aliases: []string{"ls"},
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "installed",
Aliases: []string{"I"},
},
},
Action: func(c *cli.Context) error {
ctx := c.Context
log := loggerctx.From(ctx)
pkgs, err := findPkg("*")
if err != nil {
log.Fatal("Error finding packages").Err(err).Send()
}
for _, script := range pkgs {
vars, err := getBuildVars(c.Context, script, info)
err := repos.Pull(ctx, config.Config(ctx).Repos)
if err != nil {
log.Fatal("Error getting build variables").Err(err).Send()
log.Fatal("Error pulling repositories").Err(err).Send()
}
fmt.Println(vars.Name, vars.Version)
}
where := "true"
args := []any(nil)
if c.NArg() > 0 {
where = "name LIKE ? OR json_array_contains(provides, ?)"
args = []any{c.Args().First(), c.Args().First()}
}
return nil
result, err := db.GetPkgs(ctx, where, args...)
if err != nil {
log.Fatal("Error getting packages").Err(err).Send()
}
defer result.Close()
var installed map[string]string
if c.Bool("installed") {
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect a supported package manager on the system").Send()
}
installed, err = mgr.ListInstalled(&manager.Opts{AsRoot: false})
if err != nil {
log.Fatal("Error listing installed packages").Err(err).Send()
}
}
for result.Next() {
var pkg db.Package
err := result.StructScan(&pkg)
if err != nil {
return err
}
if slices.Contains(config.Config(ctx).IgnorePkgUpdates, pkg.Name) {
continue
}
version := pkg.Version
if c.Bool("installed") {
instVersion, ok := installed[pkg.Name]
if !ok {
continue
} else {
version = instVersion
}
}
fmt.Printf("%s/%s %s\n", pkg.Repository, pkg.Name, version)
}
if err != nil {
log.Fatal("Error iterating over packages").Err(err).Send()
}
return nil
},
}

185
main.go
View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -22,127 +22,94 @@ import (
"context"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/mattn/go-isatty"
"github.com/urfave/cli/v2"
"go.arsenm.dev/logger"
"go.elara.ws/logger"
"lure.sh/lure/internal/config"
"lure.sh/lure/internal/db"
"lure.sh/lure/internal/translations"
"lure.sh/lure/pkg/loggerctx"
"lure.sh/lure/pkg/manager"
)
var log = logger.NewPretty(os.Stderr)
var app = &cli.App{
Name: "lure",
Usage: "Linux User REpository",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "pm-args",
Aliases: []string{"P"},
Usage: "Arguments to be passed on to the package manager",
},
&cli.BoolFlag{
Name: "interactive",
Aliases: []string{"i"},
Value: isatty.IsTerminal(os.Stdin.Fd()),
Usage: "Enable interactive questions and prompts",
},
},
Commands: []*cli.Command{
installCmd,
removeCmd,
upgradeCmd,
infoCmd,
listCmd,
buildCmd,
addrepoCmd,
removerepoCmd,
refreshCmd,
fixCmd,
genCmd,
helperCmd,
versionCmd,
},
Before: func(c *cli.Context) error {
ctx := c.Context
log := loggerctx.From(ctx)
cmd := c.Args().First()
if cmd != "helper" && !config.Config(ctx).Unsafe.AllowRunAsRoot && os.Geteuid() == 0 {
log.Fatal("Running LURE as root is forbidden as it may cause catastrophic damage to your system").Send()
}
if trimmed := strings.TrimSpace(c.String("pm-args")); trimmed != "" {
args := strings.Split(trimmed, " ")
manager.Args = append(manager.Args, args...)
}
return nil
},
After: func(ctx *cli.Context) error {
return db.Close()
},
EnableBashCompletion: true,
}
var versionCmd = &cli.Command{
Name: "version",
Usage: "Print the current LURE version and exit",
Action: func(ctx *cli.Context) error {
println(config.Version)
return nil
},
}
func main() {
ctx := context.Background()
log := translations.NewLogger(ctx, logger.NewCLI(os.Stderr), config.Language(ctx))
ctx = loggerctx.With(ctx, log)
// Set the root command to the one set in the LURE config
manager.DefaultRootCmd = config.Config(ctx).RootCmd
ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
defer cancel()
go func() {
<-ctx.Done()
// Exit the program after a maximum of 200ms
time.Sleep(200 * time.Millisecond)
os.Exit(0)
}()
app := &cli.App{
Name: "lure",
Usage: "Linux User REpository",
Commands: []*cli.Command{
{
Name: "install",
Usage: "Install a new package",
Aliases: []string{"in"},
Action: installCmd,
},
{
Name: "remove",
Usage: "Remove an installed package",
Aliases: []string{"rm"},
Action: removeCmd,
},
{
Name: "upgrade",
Usage: "Upgrade all installed packages",
Aliases: []string{"up"},
Action: upgradeCmd,
},
{
Name: "info",
Usage: "Print information about a package",
Action: infoCmd,
},
{
Name: "list",
Usage: "List LURE repo packages",
Aliases: []string{"ls"},
Action: listCmd,
},
{
Flags: []cli.Flag{
&cli.StringFlag{
Name: "script",
Aliases: []string{"s"},
Value: "lure.sh",
Usage: "Path to the build script",
},
},
Name: "build",
Usage: "Build a local package",
Action: buildCmd,
},
{
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Required: true,
Usage: "Name of the new repo",
},
&cli.StringFlag{
Name: "url",
Aliases: []string{"u"},
Required: true,
Usage: "URL of the new repo",
},
},
Name: "addrepo",
Usage: "Add a new repository",
Aliases: []string{"ar"},
Action: addrepoCmd,
},
{
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Required: true,
Usage: "Name of the repo to be deleted",
},
},
Name: "removerepo",
Usage: "Remove an existing repository",
Aliases: []string{"rr"},
Action: removerepoCmd,
},
{
Name: "refresh",
Usage: "Pull all repositories that have changed",
Aliases: []string{"ref"},
Action: refreshCmd,
},
{
Name: "version",
Usage: "Display the current LURE version and exit",
Action: displayVersion,
},
},
}
err := app.RunContext(ctx, os.Args)
if err != nil {
log.Error("Error while running app").Err(err).Send()
}
}
func displayVersion(c *cli.Context) error {
print(version)
return nil
}

838
pkg/build/build.go Normal file
View File

@ -0,0 +1,838 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package build
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
"time"
_ "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/goreleaser/nfpm/v2/files"
"lure.sh/lure/internal/cliutils"
"lure.sh/lure/internal/config"
"lure.sh/lure/internal/cpu"
"lure.sh/lure/internal/db"
"lure.sh/lure/internal/dl"
"lure.sh/lure/internal/shutils/decoder"
"lure.sh/lure/internal/shutils/handlers"
"lure.sh/lure/internal/shutils/helpers"
"lure.sh/lure/internal/types"
"lure.sh/lure/pkg/distro"
"lure.sh/lure/pkg/loggerctx"
"lure.sh/lure/pkg/manager"
"lure.sh/lure/pkg/repos"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
)
// BuildPackage builds the script at the given path. It returns two slices. One contains the paths
// to the built package(s), the other contains the names of the built package(s).
func BuildPackage(ctx context.Context, opts types.BuildOpts) ([]string, []string, error) {
log := loggerctx.From(ctx)
info, err := distro.ParseOSRelease(ctx)
if err != nil {
return nil, nil, err
}
fl, err := parseScript(info, opts.Script)
if err != nil {
return nil, nil, err
}
// The first pass is just used to get variable values and runs before
// the script is displayed, so it's restricted so as to prevent malicious
// code from executing.
vars, err := executeFirstPass(ctx, info, fl, opts.Script)
if err != nil {
return nil, nil, err
}
dirs := getDirs(ctx, vars, opts.Script)
// If opts.Clean isn't set and we find the package already built,
// just return it rather than rebuilding
if !opts.Clean {
builtPkgPath, ok, err := checkForBuiltPackage(opts.Manager, vars, getPkgFormat(opts.Manager), dirs.BaseDir)
if err != nil {
return nil, nil, err
}
if ok {
return []string{builtPkgPath}, nil, err
}
}
// Ask the user if they'd like to see the build script
err = cliutils.PromptViewScript(ctx, opts.Script, vars.Name, config.Config(ctx).PagerStyle, opts.Interactive)
if err != nil {
log.Fatal("Failed to prompt user to view build script").Err(err).Send()
}
log.Info("Building package").Str("name", vars.Name).Str("version", vars.Version).Send()
// The second pass will be used to execute the actual code,
// so it's unrestricted. The script has already been displayed
// to the user by this point, so it should be safe
dec, err := executeSecondPass(ctx, info, fl, dirs)
if err != nil {
return nil, nil, err
}
// Get the installed packages on the system
installed, err := 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)
}
// Prepare the directories for building
err = prepareDirs(dirs)
if err != nil {
return nil, nil, err
}
buildDeps, err := installBuildDeps(ctx, vars, opts, installed)
if err != nil {
return nil, nil, err
}
err = installOptDeps(ctx, vars, opts, installed)
if err != nil {
return nil, nil, err
}
builtPaths, builtNames, repoDeps, err := buildLUREDeps(ctx, opts, vars)
if err != nil {
return nil, nil, err
}
log.Info("Downloading sources").Send()
err = getSources(ctx, dirs, vars)
if err != nil {
return nil, nil, err
}
err = executeFunctions(ctx, dec, dirs, vars)
if err != nil {
return nil, nil, err
}
log.Info("Building package metadata").Str("name", vars.Name).Send()
pkgFormat := getPkgFormat(opts.Manager)
pkgInfo, err := buildPkgMetadata(vars, dirs, pkgFormat, append(repoDeps, builtNames...))
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
}
log.Info("Compressing package").Str("name", pkgName).Send()
err = packager.Package(pkgInfo, pkgFile)
if err != nil {
return nil, nil, err
}
err = removeBuildDeps(ctx, buildDeps, opts)
if err != nil {
return nil, nil, err
}
// Add the path and name of the package we just built to the
// appropriate slices
pkgPaths := append(builtPaths, pkgPath)
pkgNames := append(builtNames, vars.Name)
// Remove any duplicates from the pkgPaths and pkgNames.
// Duplicates can be introduced if several of the dependencies
// depend on the same packages.
pkgPaths = removeDuplicates(pkgPaths)
pkgNames = removeDuplicates(pkgNames)
return pkgPaths, pkgNames, nil
}
// parseScript parses the build script using the built-in bash implementation
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, "lure.sh")
if err != nil {
return nil, err
}
return file, nil
}
// executeFirstPass executes the parsed script in a restricted environment
// to extract the build variables without executing any actual code.
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})
runner, err := interp.New(
interp.Env(expand.ListEnviron(env...)),
interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
interp.ExecHandler(helpers.Restricted.ExecHandler(handlers.NopExec)),
interp.ReadDirHandler(handlers.RestrictedReadDir(scriptDir)),
interp.StatHandler(handlers.RestrictedStat(scriptDir)),
interp.OpenHandler(handlers.RestrictedOpen(scriptDir)),
)
if err != nil {
return nil, err
}
err = runner.Run(ctx, fl)
if err != nil {
return nil, err
}
dec := decoder.New(info, runner)
var vars types.BuildVars
err = dec.DecodeVars(&vars)
if err != nil {
return nil, err
}
return &vars, nil
}
// getDirs returns the appropriate directories for the script
func getDirs(ctx context.Context, vars *types.BuildVars, script string) types.Directories {
baseDir := filepath.Join(config.GetPaths(ctx).PkgsDir, vars.Name)
return types.Directories{
BaseDir: baseDir,
SrcDir: filepath.Join(baseDir, "src"),
PkgDir: filepath.Join(baseDir, "pkg"),
ScriptDir: filepath.Dir(script),
}
}
// executeSecondPass executes the build script for the second time, this time without any restrictions.
// It returns a decoder that can be used to retrieve functions and variables from the script.
func executeSecondPass(ctx context.Context, info *distro.OSRelease, fl *syntax.File, dirs types.Directories) (*decoder.Decoder, error) {
env := createBuildEnvVars(info, dirs)
fakeroot := handlers.FakerootExecHandler(2 * time.Second)
runner, err := interp.New(
interp.Env(expand.ListEnviron(env...)),
interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
interp.ExecHandler(helpers.Helpers.ExecHandler(fakeroot)),
)
if err != nil {
return nil, err
}
err = runner.Run(ctx, fl)
if err != nil {
return nil, err
}
return decoder.New(info, runner), nil
}
// prepareDirs prepares the directories for building.
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)
}
// performChecks checks various things on the system to ensure that the package can be installed.
func performChecks(ctx context.Context, vars *types.BuildVars, interactive bool, installed map[string]string) (bool, error) {
log := loggerctx.From(ctx)
if !cpu.IsCompatibleWith(cpu.Arch(), vars.Architectures) {
cont, err := cliutils.YesNoPrompt(ctx, "Your system's CPU architecture doesn't match this package. Do you want to build anyway?", interactive, true)
if err != nil {
return false, err
}
if !cont {
return false, nil
}
}
if instVer, ok := installed[vars.Name]; ok {
log.Warn("This package is already installed").
Str("name", vars.Name).
Str("version", instVer).
Send()
}
return true, nil
}
// installBuildDeps installs any build dependencies that aren't already installed and returns
// a slice containing the names of all the packages it installed.
func installBuildDeps(ctx context.Context, vars *types.BuildVars, opts types.BuildOpts, installed map[string]string) ([]string, error) {
log := loggerctx.From(ctx)
var buildDeps []string
if len(vars.BuildDepends) > 0 {
found, notFound, err := repos.FindPkgs(ctx, vars.BuildDepends)
if err != nil {
return nil, err
}
found = removeAlreadyInstalled(found, installed)
log.Info("Installing build dependencies").Send()
flattened := cliutils.FlattenPkgs(ctx, found, "install", opts.Interactive)
buildDeps = packageNames(flattened)
InstallPkgs(ctx, flattened, notFound, opts)
}
return buildDeps, nil
}
// installOptDeps asks the user which, if any, optional dependencies they want to install.
// If the user chooses to install any optional dependencies, it performs the installation.
func installOptDeps(ctx context.Context, vars *types.BuildVars, opts types.BuildOpts, installed map[string]string) error {
if len(vars.OptDepends) > 0 {
optDeps, err := cliutils.ChooseOptDepends(ctx, vars.OptDepends, "install", opts.Interactive)
if err != nil {
return err
}
if len(optDeps) == 0 {
return nil
}
found, notFound, err := repos.FindPkgs(ctx, optDeps)
if err != nil {
return err
}
found = removeAlreadyInstalled(found, installed)
flattened := cliutils.FlattenPkgs(ctx, found, "install", opts.Interactive)
InstallPkgs(ctx, flattened, notFound, opts)
}
return nil
}
// buildLUREDeps builds all the LURE dependencies of the package. It returns the paths and names
// of the packages it built, as well as all the dependencies it didn't find in the LURE repo so
// they can be installed from the system repos.
func buildLUREDeps(ctx context.Context, opts types.BuildOpts, vars *types.BuildVars) (builtPaths, builtNames, repoDeps []string, err error) {
log := loggerctx.From(ctx)
if len(vars.Depends) > 0 {
log.Info("Installing dependencies").Send()
found, notFound, err := repos.FindPkgs(ctx, vars.Depends)
if err != nil {
return nil, nil, nil, err
}
repoDeps = notFound
// If there are multiple options for some packages, flatten them all into a single slice
pkgs := cliutils.FlattenPkgs(ctx, found, "install", opts.Interactive)
scripts := GetScriptPaths(ctx, pkgs)
for _, script := range scripts {
newOpts := opts
newOpts.Script = script
// Build the dependency
pkgPaths, pkgNames, err := BuildPackage(ctx, newOpts)
if err != nil {
return nil, nil, nil, err
}
// Append the paths of all the built packages to builtPaths
builtPaths = append(builtPaths, pkgPaths...)
// Append the names of all the built packages to builtNames
builtNames = append(builtNames, pkgNames...)
// Append the name of the current package to builtNames
builtNames = append(builtNames, filepath.Base(filepath.Dir(script)))
}
}
// Remove any potential duplicates, which can be introduced if
// several of the dependencies depend on the same packages.
repoDeps = removeDuplicates(repoDeps)
builtPaths = removeDuplicates(builtPaths)
builtNames = removeDuplicates(builtNames)
return builtPaths, builtNames, repoDeps, nil
}
// executeFunctions executes the special LURE functions, such as version(), prepare(), etc.
func executeFunctions(ctx context.Context, dec *decoder.Decoder, dirs types.Directories, vars *types.BuildVars) (err error) {
log := loggerctx.From(ctx)
version, ok := dec.GetFunc("version")
if ok {
log.Info("Executing version()").Send()
buf := &bytes.Buffer{}
err = version(
ctx,
interp.Dir(dirs.SrcDir),
interp.StdIO(os.Stdin, buf, os.Stderr),
)
if err != nil {
return err
}
newVer := strings.TrimSpace(buf.String())
err = setVersion(ctx, dec.Runner, newVer)
if err != nil {
return err
}
vars.Version = newVer
log.Info("Updating version").Str("new", newVer).Send()
}
prepare, ok := dec.GetFunc("prepare")
if ok {
log.Info("Executing prepare()").Send()
err = prepare(ctx, interp.Dir(dirs.SrcDir))
if err != nil {
return err
}
}
build, ok := dec.GetFunc("build")
if ok {
log.Info("Executing build()").Send()
err = build(ctx, interp.Dir(dirs.SrcDir))
if err != nil {
return err
}
}
packageFn, ok := dec.GetFunc("package")
if ok {
log.Info("Executing package()").Send()
err = packageFn(ctx, interp.Dir(dirs.SrcDir))
if err != nil {
return err
}
} else {
log.Fatal("The package() function is required").Send()
}
return nil
}
// buildPkgMetadata builds the metadata for the package that's going to be built.
func buildPkgMetadata(vars *types.BuildVars, dirs types.Directories, pkgFormat string, deps []string) (*nfpm.Info, error) {
pkgInfo := &nfpm.Info{
Name: vars.Name,
Description: vars.Description,
Arch: cpu.Arch(),
Platform: "linux",
Version: vars.Version,
Release: strconv.Itoa(vars.Release),
Homepage: vars.Homepage,
License: strings.Join(vars.Licenses, ", "),
Maintainer: vars.Maintainer,
Overridables: nfpm.Overridables{
Conflicts: vars.Conflicts,
Replaces: vars.Replaces,
Provides: vars.Provides,
Depends: deps,
},
}
if pkgFormat == "apk" {
// Alpine refuses to install packages that provide themselves, so remove any such provides
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)
if err != nil {
return nil, err
}
pkgInfo.Overridables.Contents = contents
return pkgInfo, nil
}
// buildContents builds the contents section of the package, which contains the files
// that will be placed into the final package.
func buildContents(vars *types.BuildVars, dirs types.Directories) ([]*files.Content, error) {
contents := []*files.Content{}
err := filepath.Walk(dirs.PkgDir, func(path string, fi os.FileInfo, err error) error {
trimmed := strings.TrimPrefix(path, dirs.PkgDir)
if fi.IsDir() {
f, err := os.Open(path)
if err != nil {
return err
}
// If the directory is empty, skip it
_, 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 f.Close()
}
if fi.Mode()&os.ModeSymlink != 0 {
link, err := os.Readlink(path)
if err != nil {
return err
}
// Remove pkgdir from the symlink's path
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 the file is supposed to be backed up, set its type to config|noreplace
if slices.Contains(vars.Backup, trimmed) {
fileContent.Type = "config|noreplace"
}
contents = append(contents, fileContent)
return nil
})
return contents, err
}
// removeBuildDeps asks the user if they'd like to remove the build dependencies that were
// installed by installBuildDeps. If so, it uses the package manager to do that.
func removeBuildDeps(ctx context.Context, buildDeps []string, opts types.BuildOpts) error {
if len(buildDeps) > 0 {
remove, err := cliutils.YesNoPrompt(ctx, "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 tries to detect a previously-built package and returns its path
// and true if it finds one. If it doesn't find it, it returns "", 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
}
// 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 := &nfpm.Info{
Name: vars.Name,
Arch: cpu.Arch(),
Version: vars.Version,
Release: strconv.Itoa(vars.Release),
Epoch: strconv.FormatUint(uint64(vars.Epoch), 10),
}
packager, err := nfpm.Get(pkgFormat)
if err != nil {
return "", err
}
return packager.ConventionalFileName(pkgInfo), nil
}
// getPkgFormat returns the package format of the package manager,
// or LURE_PKG_FORMAT if that's set.
func getPkgFormat(mgr manager.Manager) string {
pkgFormat := mgr.Format()
if format, ok := os.LookupEnv("LURE_PKG_FORMAT"); ok {
pkgFormat = format
}
return pkgFormat
}
// createBuildEnvVars creates the environment variables that will be set in the
// build script when it's executed.
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 downloads the sources from the script.
func getSources(ctx context.Context, dirs types.Directories, bv *types.BuildVars) error {
log := loggerctx.From(ctx)
if len(bv.Sources) != len(bv.Checksums) {
log.Fatal("The checksums array must be the same length as sources").Send()
}
for i, src := range bv.Sources {
opts := dl.Options{
Name: fmt.Sprintf("%s[%d]", bv.Name, i),
URL: src,
Destination: dirs.SrcDir,
Progress: os.Stderr,
LocalDir: dirs.ScriptDir,
}
if !strings.EqualFold(bv.Checksums[i], "SKIP") {
// If the checksum contains a colon, use the part before the colon
// as the algorithm and the part after as the actual checksum.
// Otherwise, use the default sha256 with the whole string as the checksum.
algo, hashData, ok := strings.Cut(bv.Checksums[i], ":")
if ok {
checksum, err := hex.DecodeString(hashData)
if err != nil {
return err
}
opts.Hash = checksum
opts.HashAlgorithm = algo
} else {
checksum, err := hex.DecodeString(bv.Checksums[i])
if err != nil {
return err
}
opts.Hash = checksum
}
}
err := dl.Download(ctx, opts)
if err != nil {
return err
}
}
return nil
}
// setScripts adds any hook scripts to the package metadata.
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 changes the version variable in the script runner.
// It's used to set the version to the output of the version() function.
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)
}
// removeAlreadyInstalled returns a map without any dependencies that are already installed
func removeAlreadyInstalled(found map[string][]db.Package, installed map[string]string) map[string][]db.Package {
filteredPackages := make(map[string][]db.Package)
for name, pkgList := range found {
filteredPkgList := []db.Package{}
for _, pkg := range pkgList {
if _, isInstalled := installed[pkg.Name]; !isInstalled {
filteredPkgList = append(filteredPkgList, pkg)
}
}
filteredPackages[name] = filteredPkgList
}
return filteredPackages
}
// packageNames returns the names of all the given packages
func packageNames(pkgs []db.Package) []string {
names := make([]string, len(pkgs))
for i, p := range pkgs {
names[i] = p.Name
}
return names
}
// removeDuplicates removes any duplicates from the given slice
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
}

72
pkg/build/install.go Normal file
View File

@ -0,0 +1,72 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package build
import (
"context"
"path/filepath"
"lure.sh/lure/internal/config"
"lure.sh/lure/internal/db"
"lure.sh/lure/internal/types"
"lure.sh/lure/pkg/loggerctx"
)
// InstallPkgs installs native packages via the package manager,
// then builds and installs the LURE packages
func InstallPkgs(ctx context.Context, lurePkgs []db.Package, nativePkgs []string, opts types.BuildOpts) {
log := loggerctx.From(ctx)
if len(nativePkgs) > 0 {
err := opts.Manager.Install(nil, nativePkgs...)
if err != nil {
log.Fatal("Error installing native packages").Err(err).Send()
}
}
InstallScripts(ctx, GetScriptPaths(ctx, lurePkgs), opts)
}
// GetScriptPaths returns a slice of script paths corresponding to the
// given packages
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, "lure.sh")
scripts = append(scripts, scriptPath)
}
return scripts
}
// InstallScripts builds and installs the given LURE build scripts
func InstallScripts(ctx context.Context, scripts []string, opts types.BuildOpts) {
log := loggerctx.From(ctx)
for _, script := range scripts {
opts.Script = script
builtPkgs, _, err := BuildPackage(ctx, opts)
if err != nil {
log.Fatal("Error building package").Err(err).Send()
}
err = opts.Manager.InstallLocal(nil, builtPkgs...)
if err != nil {
log.Fatal("Error installing package").Err(err).Send()
}
}
}

View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -20,18 +20,16 @@ package distro
import (
"context"
"errors"
"os"
"strings"
"go.arsenm.dev/lure/internal/shutils"
"lure.sh/lure/internal/shutils/handlers"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
)
var ErrParse = errors.New("could not parse os-release file")
// OSRelease contains information from an os-release file
type OSRelease struct {
Name string
PrettyName string
@ -46,8 +44,17 @@ type OSRelease struct {
Logo string
}
// OSReleaseName returns the NAME field of the
var parsed *OSRelease
// OSReleaseName returns a struct parsed from the system's os-release
// file. It checks /etc/os-release as well as /usr/lib/os-release.
// The first time it's called, it'll parse the os-release file.
// Subsequent calls will return the same value.
func ParseOSRelease(ctx context.Context) (*OSRelease, error) {
if parsed != nil {
return parsed, nil
}
fl, err := os.Open("/usr/lib/os-release")
if err != nil {
fl, err = os.Open("/etc/os-release")
@ -67,10 +74,10 @@ func ParseOSRelease(ctx context.Context) (*OSRelease, error) {
// as well as no environment variables in order to prevent vulnerabilities
// caused by changing the os-release file.
runner, err := interp.New(
interp.OpenHandler(shutils.NopOpen),
interp.ExecHandler(shutils.NopExec),
interp.ReadDirHandler(shutils.NopReadDir),
interp.StatHandler(shutils.NopStat),
interp.OpenHandler(handlers.NopOpen),
interp.ExecHandler(handlers.NopExec),
interp.ReadDirHandler(handlers.NopReadDir),
interp.StatHandler(handlers.NopStat),
interp.Env(expand.ListEnviron()),
)
if err != nil {
@ -79,7 +86,7 @@ func ParseOSRelease(ctx context.Context) (*OSRelease, error) {
err = runner.Run(ctx, file)
if err != nil {
return nil, ErrParse
return nil, err
}
out := &OSRelease{
@ -95,9 +102,17 @@ func ParseOSRelease(ctx context.Context) (*OSRelease, error) {
Logo: runner.Vars["LOGO"].Str,
}
if runner.Vars["ID_LIKE"].IsSet() {
distroUpdated := false
if distID, ok := os.LookupEnv("LURE_DISTRO"); ok {
out.ID = distID
}
if distLike, ok := os.LookupEnv("LURE_DISTRO_LIKE"); ok {
out.Like = strings.Split(distLike, " ")
} else if runner.Vars["ID_LIKE"].IsSet() && !distroUpdated {
out.Like = strings.Split(runner.Vars["ID_LIKE"].Str, " ")
}
parsed = out
return out, nil
}

13
pkg/gen/funcs.go Normal file
View File

@ -0,0 +1,13 @@
package gen
import (
"strings"
"text/template"
)
var funcs = template.FuncMap{
"tolower": strings.ToLower,
"firstchar": func(s string) string {
return s[:1]
},
}

84
pkg/gen/pip.go Normal file
View File

@ -0,0 +1,84 @@
package gen
import (
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"text/template"
)
//go:embed tmpls/pip.tmpl.sh
var pipTmpl string
type PipOptions struct {
Name string
Version string
Description string
}
type pypiAPIResponse struct {
Info pypiInfo `json:"info"`
URLs []pypiURL `json:"urls"`
}
func (res pypiAPIResponse) SourceURL() (pypiURL, error) {
for _, url := range res.URLs {
if url.PackageType == "sdist" {
return url, nil
}
}
return pypiURL{}, errors.New("package doesn't have a source distribution")
}
type pypiInfo struct {
Name string `json:"name"`
Version string `json:"version"`
Summary string `json:"summary"`
Homepage string `json:"home_page"`
License string `json:"license"`
}
type pypiURL struct {
Digests map[string]string `json:"digests"`
Filename string `json:"filename"`
PackageType string `json:"packagetype"`
}
func Pip(w io.Writer, opts PipOptions) error {
tmpl, err := template.New("pip").
Funcs(funcs).
Parse(pipTmpl)
if err != nil {
return err
}
url := fmt.Sprintf(
"https://pypi.org/pypi/%s/%s/json",
opts.Name,
opts.Version,
)
res, err := http.Get(url)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return fmt.Errorf("pypi: %s", res.Status)
}
var resp pypiAPIResponse
err = json.NewDecoder(res.Body).Decode(&resp)
if err != nil {
return err
}
if opts.Description != "" {
resp.Info.Summary = opts.Description
}
return tmpl.Execute(w, resp)
}

31
pkg/gen/tmpls/pip.tmpl.sh Normal file
View File

@ -0,0 +1,31 @@
name='{{.Info.Name | tolower}}'
version='{{.Info.Version}}'
release='1'
desc='{{.Info.Summary}}'
homepage='{{.Info.Homepage}}'
maintainer='Example <user@example.com>'
architectures=('all')
license=('{{if .Info.License | ne ""}}{{.Info.License}}{{else}}custom:Unknown{{end}}')
provides=('{{.Info.Name | tolower}}')
conflicts=('{{.Info.Name | tolower}}')
deps=("python3")
deps_arch=("python")
deps_alpine=("python3")
build_deps=("python3" "python3-setuptools")
build_deps_arch=("python" "python-setuptools")
build_deps_alpine=("python3" "py3-setuptools")
sources=("https://files.pythonhosted.org/packages/source/{{.SourceURL.Filename | firstchar}}/{{.Info.Name}}/{{.SourceURL.Filename}}")
checksums=('blake2b-256:{{.SourceURL.Digests.blake2b_256}}')
build() {
cd "$srcdir/{{.Info.Name}}-${version}"
python3 setup.py build
}
package() {
cd "$srcdir/{{.Info.Name}}-${version}"
python3 setup.py install --root="${pkgdir}/" --optimize=1 || return 1
}

29
pkg/loggerctx/log.go Normal file
View File

@ -0,0 +1,29 @@
package loggerctx
import (
"context"
"go.elara.ws/logger"
)
// loggerCtxKey is used as the context key for loggers
type loggerCtxKey struct{}
// With returns a copy of ctx containing log
func With(ctx context.Context, log logger.Logger) context.Context {
return context.WithValue(ctx, loggerCtxKey{}, log)
}
// From attempts to get a logger from ctx. If ctx doesn't
// contain a logger, it returns a nop logger.
func From(ctx context.Context) logger.Logger {
if val := ctx.Value(loggerCtxKey{}); val != nil {
if log, ok := val.(logger.Logger); ok && log != nil {
return log
} else {
return logger.NewNop()
}
} else {
return logger.NewNop()
}
}

View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -112,15 +112,8 @@ func (a *APK) UpgradeAll(opts *Opts) error {
}
func (a *APK) ListInstalled(opts *Opts) (map[string]string, error) {
opts = ensureOpts(opts)
out := map[string]string{}
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(a.rootCmd), "apk", "list", "-I")
} else {
cmd = exec.Command("apk", "list", "-I")
}
cmd := exec.Command("apk", "list", "-I")
stdout, err := cmd.StdoutPipe()
if err != nil {
@ -159,6 +152,7 @@ func (a *APK) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(a.rootCmd), mgrCmd)
cmd.Args = append(cmd.Args, opts.Args...)
cmd.Args = append(cmd.Args, args...)
} else {
cmd = exec.Command(mgrCmd, args...)

View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -104,15 +104,8 @@ func (a *APT) UpgradeAll(opts *Opts) error {
}
func (a *APT) ListInstalled(opts *Opts) (map[string]string, error) {
opts = ensureOpts(opts)
out := map[string]string{}
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(a.rootCmd), "dpkg-query", "-f", "${Package}\u200b${Version}\\n", "-W")
} else {
cmd = exec.Command("dpkg-query", "-f", "${Package}\u200b${Version}\\n", "-W")
}
cmd := exec.Command("dpkg-query", "-f", "${Package}\u200b${Version}\\n", "-W")
stdout, err := cmd.StdoutPipe()
if err != nil {
@ -145,6 +138,7 @@ func (a *APT) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(a.rootCmd), mgrCmd)
cmd.Args = append(cmd.Args, opts.Args...)
cmd.Args = append(cmd.Args, args...)
} else {
cmd = exec.Command(mgrCmd, args...)

View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -60,7 +60,7 @@ func (d *DNF) Sync(opts *Opts) error {
func (d *DNF) Install(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := d.getCmd(opts, "dnf", "install")
cmd := d.getCmd(opts, "dnf", "install", "--allowerasing")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@ -111,15 +111,8 @@ func (d *DNF) UpgradeAll(opts *Opts) error {
}
func (d *DNF) ListInstalled(opts *Opts) (map[string]string, error) {
opts = ensureOpts(opts)
out := map[string]string{}
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(d.rootCmd), "rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
} else {
cmd = exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
}
cmd := exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
stdout, err := cmd.StdoutPipe()
if err != nil {
@ -153,6 +146,7 @@ func (d *DNF) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(d.rootCmd), mgrCmd)
cmd.Args = append(cmd.Args, opts.Args...)
cmd.Args = append(cmd.Args, args...)
} else {
cmd = exec.Command(mgrCmd, args...)

View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -23,9 +23,12 @@ import (
"os/exec"
)
var Args []string
type Opts struct {
AsRoot bool
NoConfirm bool
Args []string
}
var DefaultOpts = &Opts{
@ -114,7 +117,8 @@ func setCmdEnv(cmd *exec.Cmd) {
func ensureOpts(opts *Opts) *Opts {
if opts == nil {
return DefaultOpts
opts = DefaultOpts
}
opts.Args = append(opts.Args, Args...)
return opts
}

View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -111,15 +111,8 @@ func (p *Pacman) UpgradeAll(opts *Opts) error {
}
func (p *Pacman) ListInstalled(opts *Opts) (map[string]string, error) {
opts = ensureOpts(opts)
out := map[string]string{}
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(p.rootCmd), "pacman", "-Q")
} else {
cmd = exec.Command("pacman", "-Q")
}
cmd := exec.Command("pacman", "-Q")
stdout, err := cmd.StdoutPipe()
if err != nil {
@ -152,6 +145,7 @@ func (p *Pacman) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(p.rootCmd), mgrCmd)
cmd.Args = append(cmd.Args, opts.Args...)
cmd.Args = append(cmd.Args, args...)
} else {
cmd = exec.Command(mgrCmd, args...)

View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -60,7 +60,7 @@ func (y *YUM) Sync(opts *Opts) error {
func (y *YUM) Install(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := y.getCmd(opts, "yum", "install")
cmd := y.getCmd(opts, "yum", "install", "--allowerasing")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@ -111,15 +111,8 @@ func (y *YUM) UpgradeAll(opts *Opts) error {
}
func (y *YUM) ListInstalled(opts *Opts) (map[string]string, error) {
opts = ensureOpts(opts)
out := map[string]string{}
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(y.rootCmd), "rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
} else {
cmd = exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
}
cmd := exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
stdout, err := cmd.StdoutPipe()
if err != nil {
@ -153,6 +146,7 @@ func (y *YUM) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(y.rootCmd), mgrCmd)
cmd.Args = append(cmd.Args, opts.Args...)
cmd.Args = append(cmd.Args, args...)
} else {
cmd = exec.Command(mgrCmd, args...)

View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -111,15 +111,8 @@ func (z *Zypper) UpgradeAll(opts *Opts) error {
}
func (z *Zypper) ListInstalled(opts *Opts) (map[string]string, error) {
opts = ensureOpts(opts)
out := map[string]string{}
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(z.rootCmd), "rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
} else {
cmd = exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
}
cmd := exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
stdout, err := cmd.StdoutPipe()
if err != nil {
@ -153,6 +146,7 @@ func (z *Zypper) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(z.rootCmd), mgrCmd)
cmd.Args = append(cmd.Args, opts.Args...)
cmd.Args = append(cmd.Args, args...)
} else {
cmd = exec.Command(mgrCmd, args...)

83
pkg/repos/find.go Normal file
View File

@ -0,0 +1,83 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package repos
import (
"context"
"lure.sh/lure/internal/db"
)
// 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.
func FindPkgs(ctx context.Context, pkgs []string) (map[string][]db.Package, []string, error) {
found := map[string][]db.Package{}
notFound := []string(nil)
for _, pkgName := range pkgs {
if pkgName == "" {
continue
}
result, err := db.GetPkgs(ctx, "json_array_contains(provides, ?)", pkgName)
if err != nil {
return nil, nil, err
}
added := 0
for result.Next() {
var pkg db.Package
err = result.StructScan(&pkg)
if err != nil {
return nil, nil, err
}
added++
found[pkgName] = append(found[pkgName], pkg)
}
result.Close()
if added == 0 {
result, err := db.GetPkgs(ctx, "name LIKE ?", pkgName)
if err != nil {
return nil, nil, err
}
for result.Next() {
var pkg db.Package
err = result.StructScan(&pkg)
if err != nil {
return nil, nil, err
}
added++
found[pkgName] = append(found[pkgName], pkg)
}
result.Close()
}
if added == 0 {
notFound = append(notFound, pkgName)
}
}
return found, notFound, nil
}

148
pkg/repos/find_test.go Normal file
View File

@ -0,0 +1,148 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package repos_test
import (
"context"
"reflect"
"strings"
"testing"
"lure.sh/lure/internal/db"
"lure.sh/lure/internal/types"
"lure.sh/lure/pkg/repos"
)
func TestFindPkgs(t *testing.T) {
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer db.Close()
setCfgDirs(t)
defer removeCacheDir(t)
ctx := context.Background()
err = repos.Pull(ctx, []types.Repo{
{
Name: "default",
URL: "https://github.com/Arsen6331/lure-repo.git",
},
})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
found, notFound, err := repos.FindPkgs([]string{"itd", "nonexistentpackage1", "nonexistentpackage2"})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
if !reflect.DeepEqual(notFound, []string{"nonexistentpackage1", "nonexistentpackage2"}) {
t.Errorf("Expected 'nonexistentpackage{1,2} not to be found")
}
if len(found) != 1 {
t.Errorf("Expected 1 package found, got %d", len(found))
}
itdPkgs, ok := found["itd"]
if !ok {
t.Fatalf("Expected 'itd' packages to be found")
}
if len(itdPkgs) < 2 {
t.Errorf("Expected two 'itd' packages to be found")
}
for i, pkg := range itdPkgs {
if !strings.HasPrefix(pkg.Name, "itd") {
t.Errorf("Expected package name of all found packages to start with 'itd', got %s on element %d", pkg.Name, i)
}
}
}
func TestFindPkgsEmpty(t *testing.T) {
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer db.Close()
setCfgDirs(t)
defer removeCacheDir(t)
err = db.InsertPackage(db.Package{
Name: "test1",
Repository: "default",
Version: "0.0.1",
Release: 1,
Description: db.NewJSON(map[string]string{
"en": "Test package 1",
"ru": "Проверочный пакет 1",
}),
Provides: db.NewJSON([]string{""}),
})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
err = db.InsertPackage(db.Package{
Name: "test2",
Repository: "default",
Version: "0.0.1",
Release: 1,
Description: db.NewJSON(map[string]string{
"en": "Test package 2",
"ru": "Проверочный пакет 2",
}),
Provides: db.NewJSON([]string{"test"}),
})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
found, notFound, err := repos.FindPkgs([]string{"test", ""})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
if len(notFound) != 0 {
t.Errorf("Expected all packages to be found")
}
if len(found) != 1 {
t.Errorf("Expected 1 package found, got %d", len(found))
}
testPkgs, ok := found["test"]
if !ok {
t.Fatalf("Expected 'test' packages to be found")
}
if len(testPkgs) != 1 {
t.Errorf("Expected one 'test' package to be found, got %d", len(testPkgs))
}
if testPkgs[0].Name != "test2" {
t.Errorf("Expected 'test2' package, got '%s'", testPkgs[0].Name)
}
}

441
pkg/repos/pull.go Normal file
View File

@ -0,0 +1,441 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package repos
import (
"context"
"errors"
"io"
"net/url"
"os"
"path/filepath"
"reflect"
"strings"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/format/diff"
"github.com/pelletier/go-toml/v2"
"go.elara.ws/vercmp"
"lure.sh/lure/internal/config"
"lure.sh/lure/internal/db"
"lure.sh/lure/internal/shutils/decoder"
"lure.sh/lure/internal/shutils/handlers"
"lure.sh/lure/internal/types"
"lure.sh/lure/pkg/distro"
"lure.sh/lure/pkg/loggerctx"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
)
// Pull pulls the provided repositories. If a repo doesn't exist, it will be cloned
// and its packages will be written to the DB. If it does exist, it will be pulled.
// In this case, only changed packages will be processed if possible.
// If repos is set to nil, the repos in the LURE config will be used.
func Pull(ctx context.Context, repos []types.Repo) error {
log := loggerctx.From(ctx)
if repos == nil {
repos = config.Config(ctx).Repos
}
for _, repo := range repos {
repoURL, err := url.Parse(repo.URL)
if err != nil {
return err
}
log.Info("Pulling repository").Str("name", repo.Name).Send()
repoDir := filepath.Join(config.GetPaths(ctx).RepoDir, repo.Name)
var repoFS billy.Filesystem
gitDir := filepath.Join(repoDir, ".git")
// Only pull repos that contain valid git repos
if fi, err := os.Stat(gitDir); err == nil && fi.IsDir() {
r, err := git.PlainOpen(repoDir)
if err != nil {
return err
}
w, err := r.Worktree()
if err != nil {
return err
}
old, err := r.Head()
if err != nil {
return err
}
err = w.PullContext(ctx, &git.PullOptions{Progress: os.Stderr})
if errors.Is(err, git.NoErrAlreadyUpToDate) {
log.Info("Repository up to date").Str("name", repo.Name).Send()
} else if err != nil {
return err
}
repoFS = w.Filesystem
// Make sure the DB is created even if the repo is up to date
if !errors.Is(err, git.NoErrAlreadyUpToDate) || db.IsEmpty(ctx) {
new, err := r.Head()
if err != nil {
return err
}
// If the DB was not present at startup, that means it's
// empty. In this case, we need to update the DB fully
// rather than just incrementally.
if db.IsEmpty(ctx) {
err = processRepoFull(ctx, repo, repoDir)
if err != nil {
return err
}
} else {
err = processRepoChanges(ctx, repo, r, w, old, new)
if err != nil {
return err
}
}
}
} else {
err = os.RemoveAll(repoDir)
if err != nil {
return err
}
err = os.MkdirAll(repoDir, 0o755)
if err != nil {
return err
}
_, err = git.PlainCloneContext(ctx, repoDir, false, &git.CloneOptions{
URL: repoURL.String(),
Progress: os.Stderr,
})
if err != nil {
return err
}
err = processRepoFull(ctx, repo, repoDir)
if err != nil {
return err
}
repoFS = osfs.New(repoDir)
}
fl, err := repoFS.Open("lure-repo.toml")
if err != nil {
log.Warn("Git repository does not appear to be a valid LURE repo").Str("repo", repo.Name).Send()
continue
}
var repoCfg types.RepoConfig
err = toml.NewDecoder(fl).Decode(&repoCfg)
if err != nil {
return err
}
fl.Close()
// If the version doesn't have a "v" prefix, it's not a standard version.
// It may be "unknown" or a git version, but either way, there's no way
// to compare it to the repo version, so only compare versions with the "v".
if strings.HasPrefix(config.Version, "v") {
if vercmp.Compare(config.Version, repoCfg.Repo.MinVersion) == -1 {
log.Warn("LURE repo's minumum LURE version is greater than the current version. Try updating LURE if something doesn't work.").Str("repo", repo.Name).Send()
}
}
}
return nil
}
type actionType uint8
const (
actionDelete actionType = iota
actionUpdate
)
type action struct {
Type actionType
File string
}
func 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 {
return err
}
newCommit, err := r.CommitObject(new.Hash())
if err != nil {
return err
}
patch, err := oldCommit.Patch(newCommit)
if err != nil {
return err
}
var actions []action
for _, fp := range patch.FilePatches() {
from, to := fp.Files()
if !isValid(from, to) {
continue
}
if to == nil {
actions = append(actions, action{
Type: actionDelete,
File: from.Path(),
})
} else if from == nil {
actions = append(actions, action{
Type: actionUpdate,
File: to.Path(),
})
} else {
if from.Path() != to.Path() {
actions = append(actions,
action{
Type: actionDelete,
File: from.Path(),
},
action{
Type: actionUpdate,
File: to.Path(),
},
)
} else {
actions = append(actions, action{
Type: actionUpdate,
File: to.Path(),
})
}
}
}
repoDir := w.Filesystem.Root()
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{}),
)
if err != nil {
return err
}
switch action.Type {
case actionDelete:
if filepath.Base(action.File) != "lure.sh" {
continue
}
scriptFl, err := oldCommit.File(action.File)
if err != nil {
return nil
}
r, err := scriptFl.Reader()
if err != nil {
return nil
}
var pkg db.Package
err = parseScript(ctx, parser, runner, r, &pkg)
if err != nil {
return err
}
err = db.DeletePkgs(ctx, "name = ? AND repository = ?", pkg.Name, repo.Name)
if err != nil {
return err
}
case actionUpdate:
if filepath.Base(action.File) != "lure.sh" {
action.File = filepath.Join(filepath.Dir(action.File), "lure.sh")
}
scriptFl, err := newCommit.File(action.File)
if err != nil {
return nil
}
r, err := scriptFl.Reader()
if err != nil {
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 = db.InsertPackage(ctx, pkg)
if err != nil {
return err
}
}
}
return nil
}
// isValid makes sure the path of the file being updated is valid.
// It checks to make sure the file is not within a nested directory
// and that it is called lure.sh.
func isValid(from, to diff.File) bool {
var path string
if from != nil {
path = from.Path()
}
if to != nil {
path = to.Path()
}
match, _ := filepath.Match("*/*.sh", path)
return match
}
func processRepoFull(ctx context.Context, repo types.Repo, repoDir string) error {
glob := filepath.Join(repoDir, "/*/lure.sh")
matches, err := filepath.Glob(glob)
if err != nil {
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{}),
)
if err != nil {
return err
}
scriptFl, err := os.Open(match)
if err != nil {
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 = db.InsertPackage(ctx, pkg)
if err != nil {
return err
}
}
return nil
}
func parseScript(ctx context.Context, parser *syntax.Parser, runner *interp.Runner, r io.ReadCloser, pkg *db.Package) error {
defer r.Close()
fl, err := parser.Parse(r, "lure.sh")
if err != nil {
return err
}
runner.Reset()
err = runner.Run(ctx, fl)
if err != nil {
return err
}
d := decoder.New(&distro.OSRelease{}, runner)
d.Overrides = false
d.LikeDistros = false
return d.DecodeVars(pkg)
}
var overridable = map[string]string{
"deps": "Depends",
"build_deps": "BuildDepends",
"desc": "Description",
"homepage": "Homepage",
"maintainer": "Maintainer",
}
func resolveOverrides(runner *interp.Runner, pkg *db.Package) {
pkgVal := reflect.ValueOf(pkg).Elem()
for name, val := range runner.Vars {
for prefix, field := range overridable {
if strings.HasPrefix(name, prefix) {
override := strings.TrimPrefix(name, prefix)
override = strings.TrimPrefix(override, "_")
field := pkgVal.FieldByName(field)
varVal := field.FieldByName("Val")
varType := varVal.Type()
switch varType.Elem().String() {
case "[]string":
varVal.SetMapIndex(reflect.ValueOf(override), reflect.ValueOf(val.List))
case "string":
varVal.SetMapIndex(reflect.ValueOf(override), reflect.ValueOf(val.Str))
}
break
}
}
}
}

109
pkg/repos/pull_test.go Normal file
View File

@ -0,0 +1,109 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Elara Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package repos_test
import (
"context"
"os"
"path/filepath"
"testing"
"lure.sh/lure/internal/config"
"lure.sh/lure/internal/db"
"lure.sh/lure/internal/types"
"lure.sh/lure/pkg/repos"
)
func setCfgDirs(t *testing.T) {
t.Helper()
paths := config.GetPaths()
var err error
paths.CacheDir, err = os.MkdirTemp("/tmp", "lure-pull-test.*")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
paths.RepoDir = filepath.Join(paths.CacheDir, "repo")
paths.PkgsDir = filepath.Join(paths.CacheDir, "pkgs")
err = os.MkdirAll(paths.RepoDir, 0o755)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
err = os.MkdirAll(paths.PkgsDir, 0o755)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
paths.DBPath = filepath.Join(paths.CacheDir, "db")
}
func removeCacheDir(t *testing.T) {
t.Helper()
err := os.RemoveAll(config.GetPaths().CacheDir)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
}
func TestPull(t *testing.T) {
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer db.Close()
setCfgDirs(t)
defer removeCacheDir(t)
ctx := context.Background()
err = repos.Pull(ctx, []types.Repo{
{
Name: "default",
URL: "https://github.com/Arsen6331/lure-repo.git",
},
})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
result, err := db.GetPkgs("name LIKE 'itd%'")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
var pkgAmt int
for result.Next() {
var dbPkg db.Package
err = result.StructScan(&dbPkg)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
pkgAmt++
}
if pkgAmt < 2 {
t.Errorf("Expected 2 packages to match, got %d", pkgAmt)
}
}

167
pkg/search/search.go Normal file
View File

@ -0,0 +1,167 @@
package search
import (
"context"
"errors"
"io"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"lure.sh/lure/internal/config"
"lure.sh/lure/internal/db"
)
// Filter represents search filters.
type Filter int
// Filters
const (
FilterNone Filter = iota
FilterInRepo
FilterSupportsArch
)
// SoryBy represents a value that packages can be sorted by.
type SortBy int
// Sort values
const (
SortByNone = iota
SortByName
SortByRepo
SortByVersion
)
// Package represents a package from LURE's database
type Package struct {
Name string
Version string
Release int
Epoch uint
Description map[string]string
Homepage map[string]string
Maintainer map[string]string
Architectures []string
Licenses []string
Provides []string
Conflicts []string
Replaces []string
Depends map[string][]string
BuildDepends map[string][]string
OptDepends map[string][]string
Repository string
}
func convertPkg(p db.Package) Package {
return Package{
Name: p.Name,
Version: p.Version,
Release: p.Release,
Epoch: p.Epoch,
Description: p.Description.Val,
Homepage: p.Homepage.Val,
Maintainer: p.Maintainer.Val,
Architectures: p.Architectures.Val,
Licenses: p.Licenses.Val,
Provides: p.Provides.Val,
Conflicts: p.Conflicts.Val,
Replaces: p.Replaces.Val,
Depends: p.Depends.Val,
BuildDepends: p.BuildDepends.Val,
OptDepends: p.OptDepends.Val,
Repository: p.Repository,
}
}
// Options contains the options for a search.
type Options struct {
Filter Filter
FilterValue string
SortBy SortBy
Limit int64
Query string
}
// Search searches for packages in the database based on the given options.
func Search(ctx context.Context, opts Options) ([]Package, error) {
query := "(name LIKE ? OR description LIKE ? OR json_array_contains(provides, ?))"
args := []any{"%" + opts.Query + "%", "%" + opts.Query + "%", opts.Query}
if opts.Filter != FilterNone {
switch opts.Filter {
case FilterInRepo:
query += " AND repository = ?"
case FilterSupportsArch:
query += " AND json_array_contains(architectures, ?)"
}
args = append(args, opts.FilterValue)
}
if opts.SortBy != SortByNone {
switch opts.SortBy {
case SortByName:
query += " ORDER BY name"
case SortByRepo:
query += " ORDER BY repository"
case SortByVersion:
query += " ORDER BY version"
}
}
if opts.Limit != 0 {
query += " LIMIT " + strconv.FormatInt(opts.Limit, 10)
}
result, err := db.GetPkgs(ctx, query, args...)
if err != nil {
return nil, err
}
var out []Package
for result.Next() {
pkg := db.Package{}
err = result.StructScan(&pkg)
if err != nil {
return nil, err
}
out = append(out, convertPkg(pkg))
}
return out, err
}
// GetPkg gets a single package from the database and returns it.
func GetPkg(ctx context.Context, repo, name string) (Package, error) {
pkg, err := db.GetPkg(ctx, "name = ? AND repository = ?", name, repo)
return convertPkg(*pkg), err
}
var (
// ErrInvalidArgument is an error returned by GetScript when one of its arguments
// contain invalid characters
ErrInvalidArgument = errors.New("name and repository must not contain . or /")
// ErrScriptNotFound is returned by GetScript if it can't find the script requested
// by the user.
ErrScriptNotFound = errors.New("requested script not found")
)
// GetScript returns a reader containing the build script for a given package.
func GetScript(ctx context.Context, repo, name string) (io.ReadCloser, error) {
if strings.Contains(name, "./") || strings.ContainsAny(repo, "./") {
return nil, ErrInvalidArgument
}
scriptPath := filepath.Join(config.GetPaths(ctx).RepoDir, repo, name, "lure.sh")
fl, err := os.Open(scriptPath)
if errors.Is(err, fs.ErrNotExist) {
return nil, ErrScriptNotFound
} else if err != nil {
return nil, err
}
return fl, nil
}

367
repo.go
View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -19,277 +19,144 @@
package main
import (
"context"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/go-git/go-git/v5"
"github.com/pelletier/go-toml/v2"
"github.com/urfave/cli/v2"
"go.arsenm.dev/lure/download"
"lure.sh/lure/internal/config"
"lure.sh/lure/internal/db"
"lure.sh/lure/internal/types"
"lure.sh/lure/pkg/loggerctx"
"lure.sh/lure/pkg/repos"
"golang.org/x/exp/slices"
)
type PkgNotFoundError struct {
pkgName string
}
var addrepoCmd = &cli.Command{
Name: "addrepo",
Usage: "Add a new repository",
Aliases: []string{"ar"},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Required: true,
Usage: "Name of the new repo",
},
&cli.StringFlag{
Name: "url",
Aliases: []string{"u"},
Required: true,
Usage: "URL of the new repo",
},
},
Action: func(c *cli.Context) error {
ctx := c.Context
log := loggerctx.From(ctx)
func (p PkgNotFoundError) Error() string {
return "package '" + p.pkgName + "' could not be found in any repository"
}
name := c.String("name")
repoURL := c.String("url")
type RepoConfig struct {
Repo struct {
MinVersion string `toml:"minVersion"`
}
}
cfg := config.Config(ctx)
func addrepoCmd(c *cli.Context) error {
name := c.String("name")
repoURL := c.String("url")
for _, repo := range config.Repos {
if repo.URL == repoURL {
log.Fatal("Repo already exists").Str("name", repo.Name).Send()
}
}
config.Repos = append(config.Repos, Repo{
Name: name,
URL: repoURL,
})
cfgFl, err := os.Create(cfgPath)
if err != nil {
log.Fatal("Error opening config file").Err(err).Send()
}
err = toml.NewEncoder(cfgFl).Encode(&config)
if err != nil {
log.Fatal("Error encoding config").Err(err).Send()
}
return nil
}
func removerepoCmd(c *cli.Context) error {
name := c.String("name")
found := false
index := 0
for i, repo := range config.Repos {
if repo.Name == name {
index = i
found = true
}
}
if !found {
log.Fatal("Repo does not exist").Str("name", name).Send()
}
config.Repos = slices.Delete(config.Repos, index, index+1)
cfgFl, err := os.Create(cfgPath)
if err != nil {
log.Fatal("Error opening config file").Err(err).Send()
}
err = toml.NewEncoder(cfgFl).Encode(&config)
if err != nil {
log.Fatal("Error encoding config").Err(err).Send()
}
err = os.RemoveAll(filepath.Join(cacheDir, "repo", name))
if err != nil {
log.Fatal("Error removing repo directory").Err(err).Send()
}
return nil
}
func refreshCmd(c *cli.Context) error {
err := pullRepos(c.Context)
if err != nil {
log.Fatal("Error pulling repos").Err(err).Send()
}
return nil
}
func findPkg(pkg string) ([]string, error) {
baseRepoDir := filepath.Join(cacheDir, "repo")
var out []string
for _, repo := range config.Repos {
repoDir := filepath.Join(baseRepoDir, repo.Name)
err := os.MkdirAll(repoDir, 0o755)
if err != nil {
return nil, err
}
glob := filepath.Join(repoDir, pkg, "lure.sh")
matches, err := filepath.Glob(glob)
if err != nil {
return nil, err
}
if len(matches) == 0 {
continue
}
out = append(out, matches...)
}
if len(out) == 0 {
return nil, PkgNotFoundError{pkgName: pkg}
}
return out, nil
}
func pkgPrompt(options []string) ([]string, error) {
names := make([]string, len(options))
for i, option := range options {
pkgDir := filepath.Dir(option)
repoDir := filepath.Dir(pkgDir)
names[i] = filepath.Base(repoDir) + "/" + filepath.Base(pkgDir)
}
prompt := &survey.MultiSelect{
Options: names,
Message: "Choose which package(s) to install",
}
var choices []int
err := survey.AskOne(prompt, &choices)
if err != nil {
return nil, err
}
out := make([]string, len(choices))
for i, choiceIndex := range choices {
out[i] = options[choiceIndex]
}
return out, nil
}
func findPkgs(pkgs []string) (scripts, notFound []string) {
for _, pkg := range pkgs {
found, err := findPkg(pkg)
if _, ok := err.(PkgNotFoundError); ok {
notFound = append(notFound, pkg)
continue
}
if len(found) == 1 {
scripts = append(scripts, found...)
} else {
choices, err := pkgPrompt(found)
if err != nil {
log.Fatal("Error prompting for package choices").Err(err).Send()
for _, repo := range cfg.Repos {
if repo.URL == repoURL {
log.Fatal("Repo already exists").Str("name", repo.Name).Send()
}
scripts = append(scripts, choices...)
}
}
return
}
func pullRepos(ctx context.Context) error {
baseRepoDir := filepath.Join(cacheDir, "repo")
for _, repo := range config.Repos {
repoURL, err := url.Parse(repo.URL)
if err != nil {
return err
}
log.Info("Pulling repository").Str("name", repo.Name).Send()
repoDir := filepath.Join(baseRepoDir, repo.Name)
gitDir := filepath.Join(repoDir, ".git")
if fi, err := os.Stat(gitDir); err == nil && fi.IsDir() {
r, err := git.PlainOpen(repoDir)
if err != nil {
return err
}
w, err := r.Worktree()
if err != nil {
return err
}
err = w.PullContext(ctx, &git.PullOptions{Progress: os.Stderr})
if err == git.NoErrAlreadyUpToDate {
log.Info("Repository up to date").Str("name", repo.Name).Send()
continue
} else if err != nil {
return err
}
fl, err := w.Filesystem.Open("lure-repo.toml")
if err != nil {
log.Warn("Git repository does not appear to be a valid LURE repo").Str("repo", repo.Name).Send()
continue
}
var repoCfg RepoConfig
err = toml.NewDecoder(fl).Decode(&repoCfg)
if err != nil {
return err
}
fl.Close()
currentVer, _, _ := strings.Cut(version, "-")
if vercmp(currentVer, repoCfg.Repo.MinVersion) == -1 {
log.Warn("LURE repo's minumum LURE version is greater than the current version. Try updating LURE if something doesn't work.").Str("repo", repo.Name).Send()
}
continue
}
err = os.RemoveAll(repoDir)
if err != nil {
return err
}
err = os.MkdirAll(repoDir, 0o755)
if err != nil {
return err
}
if !strings.HasPrefix(repoURL.Scheme, "git+") {
repoURL.Scheme = "git+" + repoURL.Scheme
}
err = download.Get(ctx, download.GetOptions{
SourceURL: repoURL.String(),
Destination: repoDir,
cfg.Repos = append(cfg.Repos, types.Repo{
Name: name,
URL: repoURL,
})
cfgFl, err := os.Create(config.GetPaths(ctx).ConfigPath)
if err != nil {
return err
log.Fatal("Error opening config file").Err(err).Send()
}
fl, err := os.Open(filepath.Join(repoDir, "lure-repo.toml"))
err = toml.NewEncoder(cfgFl).Encode(cfg)
if err != nil {
log.Warn("Git repository does not appear to be a valid LURE repo").Str("repo", repo.Name).Send()
log.Fatal("Error encoding config").Err(err).Send()
}
var repoCfg RepoConfig
err = toml.NewDecoder(fl).Decode(&repoCfg)
err = repos.Pull(ctx, cfg.Repos)
if err != nil {
return err
log.Fatal("Error pulling repos").Err(err).Send()
}
fl.Close()
currentVer, _, _ := strings.Cut(version, "-")
if vercmp(currentVer, repoCfg.Repo.MinVersion) == -1 {
log.Warn("LURE repo's minumum LURE version is greater than the current version. Try updating LURE if something doesn't work.").Str("repo", repo.Name).Send()
}
}
return nil
return nil
},
}
var removerepoCmd = &cli.Command{
Name: "removerepo",
Usage: "Remove an existing repository",
Aliases: []string{"rr"},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Required: true,
Usage: "Name of the repo to be deleted",
},
},
Action: func(c *cli.Context) error {
ctx := c.Context
log := loggerctx.From(ctx)
name := c.String("name")
cfg := config.Config(ctx)
found := false
index := 0
for i, repo := range cfg.Repos {
if repo.Name == name {
index = i
found = true
}
}
if !found {
log.Fatal("Repo does not exist").Str("name", name).Send()
}
cfg.Repos = slices.Delete(cfg.Repos, index, index+1)
cfgFl, err := os.Create(config.GetPaths(ctx).ConfigPath)
if err != nil {
log.Fatal("Error opening config file").Err(err).Send()
}
err = toml.NewEncoder(cfgFl).Encode(&cfg)
if err != nil {
log.Fatal("Error encoding config").Err(err).Send()
}
err = os.RemoveAll(filepath.Join(config.GetPaths(ctx).RepoDir, name))
if err != nil {
log.Fatal("Error removing repo directory").Err(err).Send()
}
err = db.DeletePkgs(ctx, "repository = ?", name)
if err != nil {
log.Fatal("Error removing packages from database").Err(err).Send()
}
return nil
},
}
var refreshCmd = &cli.Command{
Name: "refresh",
Usage: "Pull all repositories that have changed",
Aliases: []string{"ref"},
Action: func(c *cli.Context) error {
ctx := c.Context
log := loggerctx.From(ctx)
err := repos.Pull(ctx, config.Config(ctx).Repos)
if err != nil {
log.Fatal("Error pulling repos").Err(err).Send()
}
return nil
},
}

21
scripts/completion/bash Normal file
View File

@ -0,0 +1,21 @@
#! /bin/bash
: ${PROG:=$(basename ${BASH_SOURCE})}
_cli_bash_autocomplete() {
if [[ "${COMP_WORDS[0]}" != "source" ]]; then
local cur opts base
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == "-"* ]]; then
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
else
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
fi
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
}
complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG
unset PROG

20
scripts/completion/zsh Normal file
View File

@ -0,0 +1,20 @@
#compdef lure
_cli_zsh_autocomplete() {
local -a opts
local cur
cur=${words[-1]}
if [[ "$cur" == "-"* ]]; then
opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}")
else
opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-bash-completion)}")
fi
if [[ "${opts[1]}" != "" ]]; then
_describe 'values' opts
else
_files
fi
}
compdef _cli_zsh_autocomplete lure

View File

@ -1,3 +0,0 @@
#!/bin/bash
git describe --tags > version.txt

99
scripts/install.sh Normal file
View File

@ -0,0 +1,99 @@
#!/bin/bash
# LURE - Linux User REpository
# Copyright (C) 2023 Elara Musayelyan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
info() {
echo $'\x1b[32m[INFO]\x1b[0m' $@
}
warn() {
echo $'\x1b[31m[WARN]\x1b[0m' $@
}
error() {
echo $'\x1b[31;1m[ERR]\x1b[0m' $@
exit 1
}
installPkg() {
rootCmd=""
if command -v doas &>/dev/null; then
rootCmd="doas"
elif command -v sudo &>/dev/null; then
rootCmd="sudo"
else
warn "No privilege elevation command (e.g. sudo, doas) detected"
fi
case $1 in
pacman) $rootCmd pacman --noconfirm -U ${@:2} ;;
apk) $rootCmd apk add --allow-untrusted ${@:2} ;;
zypper) $rootCmd zypper --no-gpg-checks install ${@:2} ;;
*) $rootCmd $1 install -y ${@:2} ;;
esac
}
if ! command -v curl &>/dev/null; then
error "This script requires the curl command. Please install it and run again."
fi
pkgFormat=""
pkgMgr=""
if command -v pacman &>/dev/null; then
info "Detected pacman"
pkgFormat="pkg.tar.zst"
pkgMgr="pacman"
elif command -v apt &>/dev/null; then
info "Detected apt"
pkgFormat="deb"
pkgMgr="apt"
elif command -v dnf &>/dev/null; then
info "Detected dnf"
pkgFormat="rpm"
pkgMgr="dnf"
elif command -v yum &>/dev/null; then
info "Detected yum"
pkgFormat="rpm"
pkgMgr="yum"
elif command -v zypper &>/dev/null; then
info "Detected zypper"
pkgFormat="rpm"
pkgMgr="zypper"
elif command -v apk &>/dev/null; then
info "Detected apk"
pkgFormat="apk"
pkgMgr="apk"
else
error "No supported package manager detected!"
fi
latestVersion=$(curl -sI 'https://gitea.elara.ws/lure/lure/releases/latest' | grep -io 'location: .*' | rev | cut -d '/' -f1 | rev | tr -d '[:space:]')
info "Found latest LURE version:" $latestVersion
fname="$(mktemp -u -p /tmp "lure.XXXXXXXXXX").${pkgFormat}"
url="https://gitea.elara.ws/lure/lure/releases/download/${latestVersion}/linux-user-repository-${latestVersion#v}-linux-$(uname -m).${pkgFormat}"
info "Downloading LURE package"
curl -L $url -o $fname
info "Installing LURE package"
installPkg $pkgMgr $fname
info "Cleaning up"
rm $fname
info "Done!"

View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
* Copyright (C) 2023 Elara Musayelyan
*
* 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
@ -23,75 +23,109 @@ import (
"fmt"
"github.com/urfave/cli/v2"
"go.arsenm.dev/lure/distro"
"go.arsenm.dev/lure/manager"
"lure.sh/lure/internal/config"
"lure.sh/lure/internal/db"
"lure.sh/lure/internal/types"
"lure.sh/lure/pkg/build"
"lure.sh/lure/pkg/distro"
"lure.sh/lure/pkg/loggerctx"
"lure.sh/lure/pkg/manager"
"lure.sh/lure/pkg/repos"
"go.elara.ws/vercmp"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
func upgradeCmd(c *cli.Context) error {
info, err := distro.ParseOSRelease(c.Context)
if err != nil {
log.Fatal("Error parsing os-release file").Err(err).Send()
}
var upgradeCmd = &cli.Command{
Name: "upgrade",
Usage: "Upgrade all installed packages",
Aliases: []string{"up"},
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "clean",
Aliases: []string{"c"},
Usage: "Build package from scratch even if there's an already built package available",
},
},
Action: func(c *cli.Context) error {
ctx := c.Context
log := loggerctx.From(ctx)
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect supported package manager on system").Send()
}
info, err := distro.ParseOSRelease(ctx)
if err != nil {
log.Fatal("Error parsing os-release file").Err(err).Send()
}
err = pullRepos(c.Context)
if err != nil {
log.Fatal("Error pulling repos").Err(err).Send()
}
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect a supported package manager on the system").Send()
}
updates, err := checkForUpdates(c.Context, mgr, info)
if err != nil {
log.Fatal("Error checking for updates").Err(err).Send()
}
err = repos.Pull(ctx, config.Config(ctx).Repos)
if err != nil {
log.Fatal("Error pulling repos").Err(err).Send()
}
if len(updates) > 0 {
installPkgs(c.Context, updates, mgr, false)
} else {
log.Info("There is nothing to do.").Send()
}
updates, err := checkForUpdates(ctx, mgr, info)
if err != nil {
log.Fatal("Error checking for updates").Err(err).Send()
}
return nil
if len(updates) > 0 {
build.InstallPkgs(ctx, updates, nil, types.BuildOpts{
Manager: mgr,
Clean: c.Bool("clean"),
Interactive: c.Bool("interactive"),
})
} else {
log.Info("There is nothing to do.").Send()
}
return nil
},
}
func checkForUpdates(ctx context.Context, mgr manager.Manager, info *distro.OSRelease) ([]string, error) {
func checkForUpdates(ctx context.Context, mgr manager.Manager, info *distro.OSRelease) ([]db.Package, error) {
installed, err := mgr.ListInstalled(nil)
if err != nil {
return nil, err
}
var out []string
for name, version := range installed {
scripts, err := findPkg(name)
if err != nil {
pkgNames := maps.Keys(installed)
found, _, err := repos.FindPkgs(ctx, pkgNames)
if err != nil {
return nil, err
}
var out []db.Package
for pkgName, pkgs := range found {
if slices.Contains(config.Config(ctx).IgnorePkgUpdates, pkgName) {
continue
}
// since we're not using a glob, we can assume a single item
script := scripts[0]
vars, err := getBuildVars(ctx, script, info)
if err != nil {
log.Fatal("Error getting build variables").Err(err).Send()
if len(pkgs) > 1 {
// Puts the element with the highest version first
slices.SortFunc(pkgs, func(a, b db.Package) int {
return vercmp.Compare(a.Version, b.Version)
})
}
repoVer := vars.Version
if vars.Release != 0 && vars.Epoch == 0 {
repoVer = fmt.Sprintf("%s-%d", vars.Version, vars.Release)
} else if vars.Release != 0 && vars.Epoch != 0 {
repoVer = fmt.Sprintf("%d:%s-%d", vars.Epoch, vars.Version, vars.Release)
// First element is the package we want to install
pkg := pkgs[0]
repoVer := pkg.Version
if pkg.Release != 0 && pkg.Epoch == 0 {
repoVer = fmt.Sprintf("%s-%d", pkg.Version, pkg.Release)
} else if pkg.Release != 0 && pkg.Epoch != 0 {
repoVer = fmt.Sprintf("%d:%s-%d", pkg.Epoch, pkg.Version, pkg.Release)
}
c := vercmp(repoVer, version)
c := vercmp.Compare(repoVer, installed[pkgName])
if c == 0 || c == -1 {
continue
} else if c == 1 {
out = append(out, name)
out = append(out, pkg)
}
}
return out, nil
}

View File

@ -1,170 +0,0 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
_ "embed"
"strconv"
"strings"
"golang.org/x/exp/slices"
)
//go:generate scripts/gen-version.sh
//go:embed version.txt
var version string
// vercmp compares two version strings.
// It returns 1 if v1 is greater,
// 0 if the versions are equal,
// and -1 if v2 is greater
func vercmp(v1, v2 string) int {
if v1 == v2 {
return 0
}
return sepVerCmp(sepLabel(v1), sepLabel(v2))
}
func sepVerCmp(e1, e2 []string) int {
if slices.Equal(e1, e2) {
return 0
}
// proc stores the amount of elements processed
proc := 0
for i := 0; i < len(e1); i++ {
proc++
if i >= len(e2) {
return 1
}
elem1 := e1[i]
elem2 := e2[i]
if elem1 == elem2 {
continue
}
if isNumElem(elem1) && isNumElem(elem2) {
elem1v, err := strconv.ParseInt(elem1, 10, 64)
if err != nil {
// error should never happen due to isNumElem()
panic(err)
}
elem2v, err := strconv.ParseInt(elem2, 10, 64)
if err != nil {
// error should never happen due to isNumElem()
panic(err)
}
if elem1v > elem2v {
return 1
} else if elem1v < elem2v {
return -1
}
} else if isNumElem(elem1) && isAlphaElem(elem2) {
return 1
} else if isAlphaElem(elem1) && isNumElem(elem2) {
return -1
} else if isAlphaElem(elem1) && isAlphaElem(elem2) {
if elem1 > elem2 {
return 1
} else if elem1 < elem2 {
return -1
}
}
}
if proc < len(e2) {
return -1
}
return 0
}
func sepLabel(label string) []string {
const (
other = iota
alpha
num
)
var (
curType uint8
out []string
sb strings.Builder
)
for _, char := range label {
if isNum(char) {
if curType != num && curType != other {
out = append(out, sb.String())
sb.Reset()
}
sb.WriteRune(char)
curType = num
} else if isAlpha(char) {
if curType != alpha && curType != other {
out = append(out, sb.String())
sb.Reset()
}
sb.WriteRune(char)
curType = alpha
} else {
if curType != other {
out = append(out, sb.String())
sb.Reset()
}
curType = other
}
}
if sb.Len() != 0 {
out = append(out, sb.String())
}
return out
}
func isNumElem(s string) bool {
// Check only the first rune as all elements
// should consist of the same type of rune
return isNum([]rune(s[:1])[0])
}
func isNum(r rune) bool {
return r >= '0' && r <= '9'
}
func isAlphaElem(s string) bool {
// Check only the first rune as all elements
// should consist of the same type of rune
return isAlpha([]rune(s[:1])[0])
}
func isAlpha(r rune) bool {
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
}

View File

@ -1,82 +0,0 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"testing"
"golang.org/x/exp/slices"
)
func TestSepLabel(t *testing.T) {
type item struct {
label string
expected []string
}
table := []item{
{"2.0.1", []string{"2", "0", "1"}},
{"v0.0.1", []string{"v", "0", "0", "1"}},
{"2xFg33.+f.5", []string{"2", "xFg", "33", "f", "5"}},
}
for _, it := range table {
t.Run(it.label, func(t *testing.T) {
s := sepLabel(it.label)
if !slices.Equal(s, it.expected) {
t.Errorf("Expected %v, got %v", it.expected, s)
}
})
}
}
func TestVerCmp(t *testing.T) {
type item struct {
v1, v2 string
expected int
}
table := []item{
{"1.0010", "1.9", 1},
{"1.05", "1.5", 0},
{"1.0", "1", 1},
{"1", "1.0", -1},
{"2.50", "2.5", 1},
{"FC5", "fc4", -1},
{"2a", "2.0", -1},
{"1.0", "1.fc4", 1},
{"3.0.0_fc", "3.0.0.fc", 0},
{"4.1__", "4.1+", 0},
}
for _, it := range table {
t.Run(it.v1+"/"+it.v2, func(t *testing.T) {
c := vercmp(it.v1, it.v2)
if c != it.expected {
t.Errorf("Expected %d, got %d", it.expected, c)
}
// Ensure opposite comparison gives opposite value
c = -vercmp(it.v2, it.v1)
if c != it.expected {
t.Errorf("Expected %d, got %d (opposite)", it.expected, c)
}
})
}
}