Major refactor
ci/woodpecker/push/woodpecker Pipeline was successful Details

This commit is contained in:
Elara 2023-09-19 14:28:05 -07:00
parent d59c4036ef
commit 45522e3f3a
47 changed files with 1780 additions and 1693 deletions

View File

@ -19,4 +19,4 @@ uninstall:
internal/config/version.txt:
go generate ./internal/config
.PHONY: install clean uninstall
.PHONY: install clean uninstall installmisc

760
build.go
View File

@ -19,736 +19,84 @@
package main
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
_ "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.elara.ws/logger/log"
"go.elara.ws/lure/distro"
"go.elara.ws/lure/internal/cliutils"
"go.elara.ws/lure/internal/build"
"go.elara.ws/lure/internal/config"
"go.elara.ws/lure/internal/cpu"
"go.elara.ws/lure/internal/db"
"go.elara.ws/lure/internal/dl"
"go.elara.ws/lure/internal/repos"
"go.elara.ws/lure/internal/osutils"
"go.elara.ws/lure/internal/shutils"
"go.elara.ws/lure/internal/shutils/decoder"
"go.elara.ws/lure/internal/repos"
"go.elara.ws/lure/internal/types"
"go.elara.ws/lure/manager"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
)
// 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")
if c.String("package") != "" {
script = filepath.Join(config.RepoDir, c.String("package"), "lure.sh")
}
err := repos.Pull(c.Context, gdb, cfg.Repos)
if err != nil {
log.Fatal("Error pulling repositories").Err(err).Send()
}
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect supported package manager on system").Send()
}
pkgPaths, _, err := buildPackage(c.Context, script, mgr, c.Bool("clean"), c.Bool("interactive"))
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 = osutils.Move(pkgPath, filepath.Join(wd, name))
if err != nil {
log.Fatal("Error moving the package").Err(err).Send()
}
}
return nil
}
// 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, script string, mgr manager.Manager, clean, interactive bool) ([]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()
scriptDir := filepath.Dir(script)
env := genBuildEnv(info, scriptDir)
// The first pass is just used to get variable values and runs before
// the script is displayed, so it is restricted so as to prevent malicious
// code from executing.
runner, err := interp.New(
interp.Env(expand.ListEnviron(env...)),
interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
interp.ExecHandler(rHelpers.ExecHandler(shutils.NopExec)),
interp.ReadDirHandler(shutils.RestrictedReadDir(scriptDir)),
interp.StatHandler(shutils.RestrictedStat(scriptDir)),
interp.OpenHandler(shutils.RestrictedOpen(scriptDir)),
)
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
}
var vars BuildVars
err = dec.DecodeVars(&vars)
if err != nil {
return nil, nil, err
}
baseDir := filepath.Join(config.PkgsDir, vars.Name)
srcdir := filepath.Join(baseDir, "src")
pkgdir := filepath.Join(baseDir, "pkg")
if !clean {
builtPkgPath, ok, err := checkForBuiltPackage(mgr, &vars, getPkgFormat(mgr), baseDir)
if err != nil {
return nil, nil, err
}
if ok {
return []string{builtPkgPath}, nil, err
}
}
err = cliutils.PromptViewScript(script, vars.Name, cfg.PagerStyle, interactive, translator)
if err != nil {
log.Fatal("Failed to prompt user to view build script").Err(err).Send()
}
if !archMatches(vars.Architectures) {
buildAnyway, err := cliutils.YesNoPrompt("Your system's CPU architecture doesn't match this package. Do you want to build anyway?", interactive, true, translator)
if err != nil {
return nil, nil, err
}
if !buildAnyway {
os.Exit(1)
}
}
log.Info("Building package").Str("name", vars.Name).Str("version", vars.Version).Send()
// The second pass will be used to execute the actual functions,
// so it cannot be restricted. The script has already been displayed
// to the user by this point, so it should be safe
runner, err = interp.New(
interp.Env(expand.ListEnviron(env...)),
interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
interp.ExecHandler(helpers.ExecHandler(nil)),
)
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
}
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
}
if instVer, ok := installed[vars.Name]; ok {
log.Warn("This package is already installed").
Str("name", vars.Name).
Str("version", instVer).
Send()
}
var buildDeps []string
if len(vars.BuildDepends) > 0 {
found, notFound, err := repos.FindPkgs(gdb, vars.BuildDepends)
if err != nil {
return nil, nil, err
}
found = filterBuildDeps(found, installed)
log.Info("Installing build dependencies").Send()
flattened := cliutils.FlattenPkgs(found, "install", interactive, translator)
buildDeps = packageNames(flattened)
installPkgs(ctx, flattened, notFound, mgr, clean, interactive)
}
var builtDeps, builtNames, repoDeps []string
if len(vars.Depends) > 0 {
log.Info("Installing dependencies").Send()
found, notFound, err := repos.FindPkgs(gdb, vars.Depends)
if err != nil {
return nil, nil, err
}
scripts := getScriptPaths(cliutils.FlattenPkgs(found, "install", interactive, translator))
for _, script := range scripts {
pkgPaths, pkgNames, err := buildPackage(ctx, script, mgr, clean, interactive)
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
}
err = setDirVars(ctx, runner, srcdir, pkgdir)
if err != nil {
return nil, nil, err
}
fn, ok := dec.GetFunc("version")
if ok {
log.Info("Executing version()").Send()
buf := &bytes.Buffer{}
err = fn(
ctx,
interp.Dir(srcdir),
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
}
vars.Version = newVer
log.Info("Updating version").Str("new", newVer).Send()
}
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()
}
log.Info("Building package metadata").Str("name", vars.Name).Send()
uniq(
&repoDeps,
&builtDeps,
&builtNames,
)
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: 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 {
script := c.String("script")
if c.String("package") != "" {
script = filepath.Join(config.GetPaths().RepoDir, c.String("package"), "lure.sh")
}
if vars.Epoch != 0 {
pkgInfo.Epoch = strconv.FormatUint(uint64(vars.Epoch), 10)
}
err := repos.Pull(c.Context, config.Config().Repos)
if err != nil {
log.Fatal("Error pulling repositories").Err(err).Send()
}
setScripts(&vars, pkgInfo, filepath.Dir(script))
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect a supported package manager on the system").Send()
}
if slices.Contains(vars.Architectures, "all") {
pkgInfo.Arch = "all"
}
pkgPaths, _, err := build.BuildPackage(c.Context, 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
}
link = strings.TrimPrefix(link, pkgdir)
contents = append(contents, &files.Content{
Source: link,
Destination: trimmed,
Type: "symlink",
FileInfo: &files.ContentFileInfo{
MTime: fi.ModTime(),
Mode: fi.Mode(),
},
})
return nil
}
fileContent := &files.Content{
Source: path,
Destination: trimmed,
FileInfo: &files.ContentFileInfo{
MTime: fi.ModTime(),
Mode: fi.Mode(),
Size: fi.Size(),
},
}
if slices.Contains(vars.Backup, trimmed) {
fileContent.Type = "config|noreplace"
}
contents = append(contents, fileContent)
return nil
})
pkgInfo.Overridables.Contents = contents
packager, err := nfpm.Get(getPkgFormat(mgr))
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
}
log.Info("Compressing package").Str("name", pkgName).Send()
err = packager.Package(pkgInfo, pkgFile)
if err != nil {
return nil, nil, err
}
if len(buildDeps) > 0 {
removeBuildDeps, err := cliutils.YesNoPrompt("Would you like to remove build dependencies?", interactive, false, translator)
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 checkForBuiltPackage(mgr manager.Manager, vars *BuildVars, pkgFormat, baseDir string) (string, bool, error) {
filename, err := pkgFileName(vars, pkgFormat)
if err != nil {
return "", false, err
}
pkgPath := filepath.Join(baseDir, filename)
_, err = os.Stat(pkgPath)
if err != nil {
return "", false, nil
}
return pkgPath, true, nil
}
func pkgFileName(vars *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
}
func getPkgFormat(mgr manager.Manager) string {
pkgFormat := mgr.Format()
if format, ok := os.LookupEnv("LURE_PKG_FORMAT"); ok {
pkgFormat = format
}
return pkgFormat
}
func genBuildEnv(info *distro.OSRelease, scriptdir string) []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()),
"scriptdir="+scriptdir,
)
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").Send()
}
for i, src := range bv.Sources {
opts := dl.Options{
Name: fmt.Sprintf("%s[%d]", bv.Name, i),
URL: src,
Destination: srcdir,
Progress: os.Stderr,
}
if !strings.EqualFold(bv.Checksums[i], "SKIP") {
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
}
// 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 string) error {
cmd := "srcdir='" + srcdir + "'\npkgdir='" + pkgdir + "'\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)
}
}
// archMatches checks if your system architecture matches
// one of the provided architectures
func archMatches(architectures []string) bool {
if slices.Contains(architectures, "all") {
return true
}
for _, arch := range architectures {
if strings.HasPrefix(arch, "arm") {
architectures = append(architectures, cpu.CompatibleARMReverse(arch)...)
}
}
return slices.Contains(architectures, cpu.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 filterBuildDeps(found map[string][]db.Package, installed map[string]string) map[string][]db.Package {
out := map[string][]db.Package{}
for name, pkgs := range found {
var inner []db.Package
for _, pkg := range pkgs {
if _, ok := installed[pkg.Name]; !ok {
addToFiltered := true
for _, provides := range pkg.Provides.Val {
if _, ok := installed[provides]; ok {
addToFiltered = false
break
}
}
if addToFiltered {
inner = append(inner, pkg)
}
}
}
if len(inner) > 0 {
out[name] = inner
}
}
return out
}
func packageNames(pkgs []db.Package) []string {
names := make([]string, len(pkgs))
for i, p := range pkgs {
names[i] = p.Name
}
return names
}
// uniq removes all duplicates from string slices
func uniq(ss ...*[]string) {
for _, s := range ss {
slices.Sort(*s)
*s = slices.Compact(*s)
}
},
}

View File

@ -25,7 +25,6 @@ import (
"strconv"
"strings"
"github.com/jmoiron/sqlx"
"github.com/twitchtv/twirp"
"go.elara.ws/logger/log"
"go.elara.ws/lure/internal/api"
@ -34,9 +33,7 @@ import (
"golang.org/x/text/language"
)
type lureWebAPI struct {
db *sqlx.DB
}
type lureWebAPI struct{}
func (l lureWebAPI) Search(ctx context.Context, req *api.SearchRequest) (*api.SearchResponse, error) {
query := "(name LIKE ? OR description LIKE ? OR json_array_contains(provides, ?))"
@ -67,7 +64,7 @@ func (l lureWebAPI) Search(ctx context.Context, req *api.SearchRequest) (*api.Se
query += " LIMIT " + strconv.FormatInt(req.Limit, 10)
}
result, err := db.GetPkgs(l.db, query, args...)
result, err := db.GetPkgs(query, args...)
if err != nil {
return nil, err
}
@ -86,7 +83,7 @@ func (l lureWebAPI) Search(ctx context.Context, req *api.SearchRequest) (*api.Se
}
func (l lureWebAPI) GetPkg(ctx context.Context, req *api.GetPackageRequest) (*api.Package, error) {
pkg, err := db.GetPkg(l.db, "name = ? AND repository = ?", req.Name, req.Repository)
pkg, err := db.GetPkg("name = ? AND repository = ?", req.Name, req.Repository)
if err != nil {
return nil, err
}
@ -98,7 +95,7 @@ func (l lureWebAPI) GetBuildScript(ctx context.Context, req *api.GetBuildScriptR
return nil, twirp.NewError(twirp.InvalidArgument, "name and repository must not contain . or /")
}
scriptPath := filepath.Join(config.RepoDir, req.Repository, req.Name, "lure.sh")
scriptPath := filepath.Join(config.GetPaths().RepoDir, req.Repository, req.Name, "lure.sh")
_, err := os.Stat(scriptPath)
if os.IsNotExist(err) {
return nil, twirp.NewError(twirp.NotFound, "requested package not found")

View File

@ -8,7 +8,6 @@ import (
"strings"
"github.com/go-chi/chi/v5"
"github.com/jmoiron/sqlx"
"go.elara.ws/lure/internal/db"
)
@ -17,12 +16,12 @@ var logoData string
var _ http.HandlerFunc
func handleBadge(gdb *sqlx.DB) http.HandlerFunc {
func handleBadge() http.HandlerFunc {
return func(res http.ResponseWriter, req *http.Request) {
repo := chi.URLParam(req, "repo")
name := chi.URLParam(req, "pkg")
pkg, err := db.GetPkg(gdb, "name = ? AND repository = ?", name, repo)
pkg, err := db.GetPkg("name = ? AND repository = ?", name, repo)
if err != nil {
http.Error(res, err.Error(), http.StatusInternalServerError)
return

View File

@ -1,34 +0,0 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 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 (
"go.elara.ws/logger/log"
"go.elara.ws/lure/internal/config"
"go.elara.ws/lure/internal/types"
)
var cfg types.Config
func init() {
err := config.Decode(&cfg)
if err != nil {
log.Fatal("Error decoding config file").Err(err).Send()
}
}

View File

@ -1,36 +0,0 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 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 (
"github.com/jmoiron/sqlx"
"go.elara.ws/logger/log"
"go.elara.ws/lure/internal/config"
"go.elara.ws/lure/internal/db"
)
var gdb *sqlx.DB
func init() {
var err error
gdb, err = db.Open(config.DBPath)
if err != nil {
log.Fatal("Error opening database").Err(err).Send()
}
}

View File

@ -30,6 +30,7 @@ import (
"go.elara.ws/logger"
"go.elara.ws/logger/log"
"go.elara.ws/lure/internal/api"
"go.elara.ws/lure/internal/config"
"go.elara.ws/lure/internal/repos"
)
@ -54,7 +55,7 @@ func main() {
log.Logger = logger.NewMulti(log.Logger, logger.NewJSON(fl))
}
err := repos.Pull(ctx, gdb, cfg.Repos)
err := repos.Pull(ctx, config.Config().Repos)
if err != nil {
log.Fatal("Error pulling repositories").Err(err).Send()
}
@ -63,14 +64,14 @@ func main() {
go repoPullWorker(ctx, sigCh)
apiServer := api.NewAPIServer(
lureWebAPI{db: gdb},
lureWebAPI{},
twirp.WithServerPathPrefix(""),
)
r := chi.NewRouter()
r.With(allowAllCORSHandler, withAcceptLanguage).Handle("/*", apiServer)
r.Post("/webhook", handleWebhook(sigCh))
r.Get("/badge/{repo}/{pkg}", handleBadge(gdb))
r.Get("/badge/{repo}/{pkg}", handleBadge())
ln, err := net.Listen("tcp", *addr)
if err != nil {

View File

@ -30,6 +30,7 @@ import (
"strings"
"go.elara.ws/logger/log"
"go.elara.ws/lure/internal/config"
"go.elara.ws/lure/internal/repos"
)
@ -87,7 +88,7 @@ func repoPullWorker(ctx context.Context, sigCh <-chan struct{}) {
for {
select {
case <-sigCh:
err := repos.Pull(ctx, gdb, cfg.Repos)
err := repos.Pull(ctx, config.Config().Repos)
if err != nil {
log.Warn("Error while pulling repositories").Err(err).Send()
}

View File

@ -1,36 +0,0 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 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 (
"go.elara.ws/logger/log"
"go.elara.ws/lure/internal/config"
"go.elara.ws/lure/internal/types"
"go.elara.ws/lure/manager"
)
var cfg types.Config
func init() {
err := config.Decode(&cfg)
if err != nil {
log.Fatal("Error decoding config file").Err(err).Send()
}
manager.DefaultRootCmd = cfg.RootCmd
}

36
db.go
View File

@ -1,36 +0,0 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 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 (
"github.com/jmoiron/sqlx"
"go.elara.ws/lure/internal/config"
"go.elara.ws/lure/internal/db"
)
var gdb *sqlx.DB
func loadDB() error {
ldb, err := db.Open(config.DBPath)
if err != nil {
return err
}
gdb = ldb
return nil
}

View File

@ -103,7 +103,14 @@ 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, " ")
}

48
fix.go
View File

@ -28,36 +28,34 @@ import (
"go.elara.ws/lure/internal/repos"
)
func fixCmd(c *cli.Context) error {
gdb.Close()
var fixCmd = &cli.Command{
Name: "fix",
Usage: "Attempt to fix problems with LURE",
Action: func(c *cli.Context) error {
db.Close()
paths := config.GetPaths()
log.Info("Removing cache directory").Send()
log.Info("Removing cache directory").Send()
err := os.RemoveAll(config.CacheDir)
if err != nil {
log.Fatal("Unable to remove cache directory").Err(err).Send()
}
err := os.RemoveAll(paths.CacheDir)
if err != nil {
log.Fatal("Unable to remove cache directory").Err(err).Send()
}
log.Info("Rebuilding cache").Send()
log.Info("Rebuilding cache").Send()
err = os.MkdirAll(config.CacheDir, 0o755)
if err != nil {
log.Fatal("Unable to create new cache directory").Err(err).Send()
}
err = os.MkdirAll(paths.CacheDir, 0o755)
if err != nil {
log.Fatal("Unable to create new cache directory").Err(err).Send()
}
// Make sure the DB is rebuilt when repos are pulled
gdb, err = db.Open(config.DBPath)
if err != nil {
log.Fatal("Error initializing database").Err(err).Send()
}
config.DBPresent = false
err = repos.Pull(c.Context, config.Config().Repos)
if err != nil {
log.Fatal("Error pulling repos").Err(err).Send()
}
err = repos.Pull(c.Context, gdb, cfg.Repos)
if err != nil {
log.Fatal("Error pulling repos").Err(err).Send()
}
log.Info("Done").Send()
log.Info("Done").Send()
return nil
return nil
},
}

105
info.go
View File

@ -33,61 +33,72 @@ import (
"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()
}
err := repos.Pull(c.Context, gdb, cfg.Repos)
if err != nil {
log.Fatal("Error pulling repositories").Err(err).Send()
}
found, _, err := repos.FindPkgs(gdb, args.Slice())
if err != nil {
log.Fatal("Error finding packages").Err(err).Send()
}
if len(found) == 0 {
os.Exit(1)
}
pkgs := cliutils.FlattenPkgs(found, "show", c.Bool("interactive"), translator)
var names []string
all := c.Bool("all")
if !all {
info, err := distro.ParseOSRelease(c.Context)
if err != nil {
log.Fatal("Error parsing os-release file").Err(err).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 {
args := c.Args()
if args.Len() < 1 {
log.Fatalf("Command info expected at least 1 argument, got %d", args.Len()).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 {
err := repos.Pull(c.Context, config.Config().Repos)
if err != nil {
log.Fatal("Error pulling repositories").Err(err).Send()
}
found, _, err := repos.FindPkgs(args.Slice())
if err != nil {
log.Fatal("Error finding packages").Err(err).Send()
}
if len(found) == 0 {
os.Exit(1)
}
pkgs := cliutils.FlattenPkgs(found, "show", c.Bool("interactive"))
var names []string
all := c.Bool("all")
if !all {
err = yaml.NewEncoder(os.Stdout).Encode(overrides.ResolvePackage(&pkg, names))
info, err := distro.ParseOSRelease(c.Context)
if err != nil {
log.Fatal("Error encoding script variables").Err(err).Send()
log.Fatal("Error parsing os-release file").Err(err).Send()
}
} else {
err = yaml.NewEncoder(os.Stdout).Encode(pkg)
names, err = overrides.Resolve(
info,
overrides.DefaultOpts.
WithLanguages([]string{config.SystemLang()}),
)
if err != nil {
log.Fatal("Error encoding script variables").Err(err).Send()
log.Fatal("Error resolving overrides").Err(err).Send()
}
}
fmt.Println("---")
}
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()
}
}
return nil
fmt.Println("---")
}
return nil
},
}

View File

@ -19,98 +19,98 @@
package main
import (
"context"
"path/filepath"
"go.elara.ws/logger/log"
"fmt"
"github.com/urfave/cli/v2"
"go.elara.ws/logger/log"
"go.elara.ws/lure/internal/build"
"go.elara.ws/lure/internal/cliutils"
"go.elara.ws/lure/internal/config"
"go.elara.ws/lure/internal/db"
"go.elara.ws/lure/internal/repos"
"go.elara.ws/lure/internal/types"
"go.elara.ws/lure/manager"
)
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()
}
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect supported package manager on system").Send()
}
err := repos.Pull(c.Context, gdb, cfg.Repos)
if err != nil {
log.Fatal("Error pulling repositories").Err(err).Send()
}
found, notFound, err := repos.FindPkgs(gdb, args.Slice())
if err != nil {
log.Fatal("Error finding packages").Err(err).Send()
}
installPkgs(c.Context, cliutils.FlattenPkgs(found, "install", c.Bool("interactive"), translator), notFound, mgr, c.Bool("clean"), c.Bool("interactive"))
return nil
}
// installPkgs installs non-LURE packages via the package manager, then builds and installs LURE
// packages
func installPkgs(ctx context.Context, pkgs []db.Package, notFound []string, mgr manager.Manager, clean, interactive bool) {
if len(notFound) > 0 {
err := mgr.Install(nil, notFound...)
if err != nil {
log.Fatal("Error installing native packages").Err(err).Send()
}
}
installScripts(ctx, mgr, getScriptPaths(pkgs), clean, interactive)
}
// getScriptPaths generates a slice of script paths corresponding to the
// given packages
func getScriptPaths(pkgs []db.Package) []string {
var scripts []string
for _, pkg := range pkgs {
scriptPath := filepath.Join(config.RepoDir, pkg.Repository, pkg.Name, "lure.sh")
scripts = append(scripts, scriptPath)
}
return scripts
}
// installScripts builds and installs LURE build scripts
func installScripts(ctx context.Context, mgr manager.Manager, scripts []string, clean, interactive bool) {
for _, script := range scripts {
builtPkgs, _, err := buildPackage(ctx, script, mgr, clean, interactive)
if err != nil {
log.Fatal("Error building package").Err(err).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 {
args := c.Args()
if args.Len() < 1 {
log.Fatalf("Command install expected at least 1 argument, got %d", args.Len()).Send()
}
err = mgr.InstallLocal(nil, builtPkgs...)
if err != nil {
log.Fatal("Error installing package").Err(err).Send()
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect a supported package manager on the system").Send()
}
}
err := repos.Pull(c.Context, config.Config().Repos)
if err != nil {
log.Fatal("Error pulling repositories").Err(err).Send()
}
found, notFound, err := repos.FindPkgs(args.Slice())
if err != nil {
log.Fatal("Error finding packages").Err(err).Send()
}
pkgs := cliutils.FlattenPkgs(found, "install", c.Bool("interactive"))
build.InstallPkgs(c.Context, pkgs, notFound, types.BuildOpts{
Manager: mgr,
Clean: c.Bool("clean"),
Interactive: c.Bool("interactive"),
})
return nil
},
BashComplete: func(c *cli.Context) {
result, err := db.GetPkgs("true")
if err != nil {
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 {
args := c.Args()
if args.Len() < 1 {
log.Fatalf("Command remove expected at least 1 argument, got %d", args.Len()).Send()
}
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect supported package manager on system").Send()
}
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect a supported package manager on the system").Send()
}
err := mgr.Remove(nil, c.Args().Slice()...)
if err != nil {
log.Fatal("Error removing packages").Err(err).Send()
}
err := mgr.Remove(nil, c.Args().Slice()...)
if err != nil {
log.Fatal("Error removing packages").Err(err).Send()
}
return nil
return nil
},
}

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v3.21.9
// protoc v4.24.2
// source: lure.proto
package api

728
internal/build/build.go Normal file
View File

@ -0,0 +1,728 @@
package build
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
"github.com/goreleaser/nfpm/v2"
"github.com/goreleaser/nfpm/v2/files"
"go.elara.ws/logger/log"
"go.elara.ws/lure/distro"
"go.elara.ws/lure/internal/cliutils"
"go.elara.ws/lure/internal/config"
"go.elara.ws/lure/internal/cpu"
"go.elara.ws/lure/internal/db"
"go.elara.ws/lure/internal/dl"
"go.elara.ws/lure/internal/repos"
"go.elara.ws/lure/internal/shutils"
"go.elara.ws/lure/internal/shutils/decoder"
"go.elara.ws/lure/internal/shutils/helpers"
"go.elara.ws/lure/internal/types"
"go.elara.ws/lure/manager"
"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) {
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
}
vars, err := executeFirstPass(ctx, info, fl, opts.Script)
if err != nil {
return nil, nil, err
}
dirs := getDirs(vars, opts.Script)
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
}
}
err = cliutils.PromptViewScript(opts.Script, vars.Name, config.Config().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()
dec, err := executeSecondPass(ctx, info, fl, dirs)
if err != nil {
return nil, nil, err
}
installed, err := opts.Manager.ListInstalled(nil)
if err != nil {
return nil, nil, err
}
cont, err := performChecks(vars, opts.Interactive, installed)
if err != nil {
return nil, nil, err
} else if !cont {
os.Exit(1)
}
err = prepareDirs(dirs)
if err != nil {
return nil, nil, err
}
buildDeps, err := installBuildDeps(ctx, vars, opts, installed)
if err != nil {
return nil, nil, err
}
builtPaths, builtNames, repoDeps, err := installDeps(ctx, opts, vars)
if err != nil {
return nil, nil, err
}
log.Info("Downloading sources").Send()
err = getSources(ctx, dirs.SrcDir, 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()
pkgInfo, err := buildPkgMetadata(vars, dirs, append(repoDeps, builtNames...))
if err != nil {
return nil, nil, err
}
packager, err := nfpm.Get(getPkgFormat(opts.Manager))
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(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)
pkgPaths = removeDuplicates(pkgPaths)
pkgNames = removeDuplicates(pkgNames)
return pkgPaths, pkgNames, nil
}
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
}
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})
// The first pass is just used to get variable values and runs before
// the script is displayed, so it is restricted so as to prevent malicious
// code from executing.
runner, err := interp.New(
interp.Env(expand.ListEnviron(env...)),
interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
interp.ExecHandler(helpers.Restricted.ExecHandler(shutils.NopExec)),
interp.ReadDirHandler(shutils.RestrictedReadDir(scriptDir)),
interp.StatHandler(shutils.RestrictedStat(scriptDir)),
interp.OpenHandler(shutils.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
}
func getDirs(vars *types.BuildVars, script string) types.Directories {
baseDir := filepath.Join(config.GetPaths().PkgsDir, vars.Name)
return types.Directories{
BaseDir: baseDir,
SrcDir: filepath.Join(baseDir, "src"),
PkgDir: filepath.Join(baseDir, "pkg"),
ScriptDir: filepath.Dir(script),
}
}
func executeSecondPass(ctx context.Context, info *distro.OSRelease, fl *syntax.File, dirs types.Directories) (*decoder.Decoder, error) {
env := createBuildEnvVars(info, dirs)
// The second pass will be used to execute the actual functions,
// so it cannot be restricted. The script has already been displayed
// to the user by this point, so it should be safe
runner, err := interp.New(
interp.Env(expand.ListEnviron(env...)),
interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
interp.ExecHandler(helpers.Helpers.ExecHandler(nil)),
)
if err != nil {
return nil, err
}
err = runner.Run(ctx, fl)
if err != nil {
return nil, err
}
return decoder.New(info, runner), nil
}
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)
}
func performChecks(vars *types.BuildVars, interactive bool, installed map[string]string) (bool, error) {
if !cpu.IsCompatibleWith(cpu.Arch(), vars.Architectures) {
cont, err := cliutils.YesNoPrompt("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
}
func installBuildDeps(ctx context.Context, vars *types.BuildVars, opts types.BuildOpts, installed map[string]string) ([]string, error) {
var buildDeps []string
if len(vars.BuildDepends) > 0 {
found, notFound, err := repos.FindPkgs(vars.BuildDepends)
if err != nil {
return nil, err
}
found = filterBuildDeps(found, installed)
log.Info("Installing build dependencies").Send()
flattened := cliutils.FlattenPkgs(found, "install", opts.Interactive)
buildDeps = packageNames(flattened)
InstallPkgs(ctx, flattened, notFound, opts)
}
return buildDeps, nil
}
func installDeps(ctx context.Context, opts types.BuildOpts, vars *types.BuildVars) (builtPaths, builtNames, repoDeps []string, err error) {
if len(vars.Depends) > 0 {
log.Info("Installing dependencies").Send()
found, notFound, err := repos.FindPkgs(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(found, "install", opts.Interactive)
scripts := GetScriptPaths(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)))
}
}
repoDeps = removeDuplicates(repoDeps)
builtPaths = removeDuplicates(builtPaths)
builtNames = removeDuplicates(builtNames)
return builtPaths, builtNames, repoDeps, nil
}
func executeFunctions(ctx context.Context, dec *decoder.Decoder, dirs types.Directories, vars *types.BuildVars) (err error) {
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
}
func buildPkgMetadata(vars *types.BuildVars, dirs types.Directories, 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 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
}
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
}
_, 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
}
link = strings.TrimPrefix(link, dirs.PkgDir)
contents = append(contents, &files.Content{
Source: link,
Destination: trimmed,
Type: "symlink",
FileInfo: &files.ContentFileInfo{
MTime: fi.ModTime(),
Mode: fi.Mode(),
},
})
return nil
}
fileContent := &files.Content{
Source: path,
Destination: trimmed,
FileInfo: &files.ContentFileInfo{
MTime: fi.ModTime(),
Mode: fi.Mode(),
Size: fi.Size(),
},
}
if slices.Contains(vars.Backup, trimmed) {
fileContent.Type = "config|noreplace"
}
contents = append(contents, fileContent)
return nil
})
return contents, err
}
func removeBuildDeps(buildDeps []string, opts types.BuildOpts) error {
if len(buildDeps) > 0 {
removeBuildDeps, err := cliutils.YesNoPrompt("Would you like to remove the build dependencies?", opts.Interactive, false)
if err != nil {
return err
}
if removeBuildDeps {
err = opts.Manager.Remove(
&manager.Opts{
AsRoot: true,
NoConfirm: true,
},
buildDeps...,
)
if err != nil {
return err
}
}
}
return nil
}
func checkForBuiltPackage(mgr manager.Manager, vars *types.BuildVars, pkgFormat, baseDir string) (string, bool, error) {
filename, err := pkgFileName(vars, pkgFormat)
if err != nil {
return "", false, err
}
pkgPath := filepath.Join(baseDir, filename)
_, err = os.Stat(pkgPath)
if err != nil {
return "", false, nil
}
return pkgPath, true, nil
}
func 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
}
func getPkgFormat(mgr manager.Manager) string {
pkgFormat := mgr.Format()
if format, ok := os.LookupEnv("LURE_PKG_FORMAT"); ok {
pkgFormat = format
}
return pkgFormat
}
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
}
func getSources(ctx context.Context, srcdir string, bv *types.BuildVars) error {
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: srcdir,
Progress: os.Stderr,
}
if !strings.EqualFold(bv.Checksums[i], "SKIP") {
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
}
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)
}
}
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)
}
// filterBuildDeps returns a map without any dependencies that are already installed
func filterBuildDeps(found map[string][]db.Package, installed map[string]string) map[string][]db.Package {
out := map[string][]db.Package{}
for name, pkgs := range found {
var inner []db.Package
for _, pkg := range pkgs {
if _, ok := installed[pkg.Name]; !ok {
addToFiltered := true
for _, provides := range pkg.Provides.Val {
if _, ok := installed[provides]; ok {
addToFiltered = false
break
}
}
if addToFiltered {
inner = append(inner, pkg)
}
}
}
if len(inner) > 0 {
out[name] = inner
}
}
return out
}
func packageNames(pkgs []db.Package) []string {
names := make([]string, len(pkgs))
for i, p := range pkgs {
names[i] = p.Name
}
return names
}
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
}

51
internal/build/install.go Normal file
View File

@ -0,0 +1,51 @@
package build
import (
"context"
"path/filepath"
"go.elara.ws/logger/log"
"go.elara.ws/lure/internal/config"
"go.elara.ws/lure/internal/db"
"go.elara.ws/lure/internal/types"
)
// InstallPkgs installs non-LURE packages via the package manager, then builds and installs LURE
// packages
func InstallPkgs(ctx context.Context, lurePkgs []db.Package, nativePkgs []string, opts types.BuildOpts) {
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(lurePkgs), opts)
}
// GetScriptPaths generates a slice of script paths corresponding to the
// given packages
func GetScriptPaths(pkgs []db.Package) []string {
var scripts []string
for _, pkg := range pkgs {
scriptPath := filepath.Join(config.GetPaths().RepoDir, pkg.Repository, pkg.Name, "lure.sh")
scripts = append(scripts, scriptPath)
}
return scripts
}
// InstallScripts builds and installs LURE build scripts
func InstallScripts(ctx context.Context, scripts []string, opts types.BuildOpts) {
for _, script := range scripts {
opts.Script = script
builtPkgs, _, err := BuildPackage(ctx, opts)
if err != nil {
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

@ -26,16 +26,16 @@ import (
"go.elara.ws/lure/internal/config"
"go.elara.ws/lure/internal/db"
"go.elara.ws/lure/internal/pager"
"go.elara.ws/translate"
"go.elara.ws/lure/internal/translations"
)
// YesNoPrompt asks the user a yes or no question, using def as the default answer
func YesNoPrompt(msg string, interactive, def bool, t translate.Translator) (bool, error) {
func YesNoPrompt(msg string, interactive, def bool) (bool, error) {
if interactive {
var answer bool
err := survey.AskOne(
&survey.Confirm{
Message: t.TranslateTo(msg, config.Language),
Message: translations.Translator().TranslateTo(msg, config.Language()),
Default: def,
},
&answer,
@ -49,12 +49,13 @@ func YesNoPrompt(msg string, interactive, def bool, t translate.Translator) (boo
// 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(script, name, style string, interactive bool, t translate.Translator) error {
func PromptViewScript(script, name, style string, interactive bool) error {
if !interactive {
return nil
}
view, err := YesNoPrompt(t.TranslateTo("Would you like to view the build script for", config.Language)+" "+name, interactive, false, t)
scriptPrompt := translations.Translator().TranslateTo("Would you like to view the build script for", config.Language()) + " " + name
view, err := YesNoPrompt(scriptPrompt, interactive, false)
if err != nil {
return err
}
@ -65,13 +66,13 @@ func PromptViewScript(script, name, style string, interactive bool, t translate.
return err
}
cont, err := YesNoPrompt("Would you still like to continue?", interactive, false, t)
cont, err := YesNoPrompt("Would you still like to continue?", interactive, false)
if err != nil {
return err
}
if !cont {
log.Fatal(t.TranslateTo("User chose not to continue after reading script", config.Language)).Send()
log.Fatal(translations.Translator().TranslateTo("User chose not to continue after reading script", config.Language())).Send()
}
}
@ -98,11 +99,11 @@ func ShowScript(path, name, style string) error {
// 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(found map[string][]db.Package, verb string, interactive bool, t translate.Translator) []db.Package {
func FlattenPkgs(found map[string][]db.Package, verb string, interactive bool) []db.Package {
var outPkgs []db.Package
for _, pkgs := range found {
if len(pkgs) > 1 && interactive {
choices, err := PkgPrompt(pkgs, verb, interactive, t)
choices, err := PkgPrompt(pkgs, verb, interactive)
if err != nil {
log.Fatal("Error prompting for choice of package").Send()
}
@ -116,7 +117,7 @@ func FlattenPkgs(found map[string][]db.Package, verb string, interactive bool, t
// PkgPrompt asks the user to choose between multiple packages.
// The user may choose multiple packages.
func PkgPrompt(options []db.Package, verb string, interactive bool, t translate.Translator) ([]db.Package, error) {
func PkgPrompt(options []db.Package, verb string, interactive bool) ([]db.Package, error) {
if !interactive {
return []db.Package{options[0]}, nil
}
@ -128,7 +129,7 @@ func PkgPrompt(options []db.Package, verb string, interactive bool, t translate.
prompt := &survey.MultiSelect{
Options: names,
Message: t.TranslateTo("Choose which package(s) to "+verb, config.Language),
Message: translations.Translator().TranslateTo("Choose which package(s) to "+verb, config.Language()),
}
var choices []int

View File

@ -22,10 +22,11 @@ import (
"os"
"github.com/pelletier/go-toml/v2"
"go.elara.ws/logger/log"
"go.elara.ws/lure/internal/types"
)
var defaultConfig = types.Config{
var defaultConfig = &types.Config{
RootCmd: "sudo",
PagerStyle: "native",
IgnorePkgUpdates: []string{},
@ -37,18 +38,30 @@ var defaultConfig = types.Config{
},
}
// Decode decodes the config file into the given
// pointer
func Decode(cfg *types.Config) error {
cfgFl, err := os.Open(ConfigPath)
if err != nil {
return err
}
defer cfgFl.Close()
var config *types.Config
// Write defaults to pointer in case some values are not set in the config
*cfg = defaultConfig
// Set repos to nil so as to avoid a duplicate default
cfg.Repos = nil
return toml.NewDecoder(cfgFl).Decode(cfg)
func Config() *types.Config {
if config == nil {
cfgFl, err := os.Open(GetPaths().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
}

View File

@ -26,69 +26,69 @@ import (
"go.elara.ws/logger/log"
)
var (
type Paths struct {
ConfigDir string
ConfigPath string
CacheDir string
RepoDir string
PkgsDir string
DBPath string
)
// DBPresent is true if the database
// was present when LURE was started
var DBPresent bool
func init() {
cfgDir, err := os.UserConfigDir()
if err != nil {
log.Fatal("Unable to detect user config directory").Err(err).Send()
}
ConfigDir = filepath.Join(cfgDir, "lure")
err = os.MkdirAll(ConfigDir, 0o755)
if err != nil {
log.Fatal("Unable to create LURE config directory").Err(err).Send()
}
ConfigPath = filepath.Join(ConfigDir, "lure.toml")
if _, err := os.Stat(ConfigPath); err != nil {
cfgFl, err := os.Create(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()
}
CacheDir = filepath.Join(cacheDir, "lure")
RepoDir = filepath.Join(CacheDir, "repo")
PkgsDir = filepath.Join(CacheDir, "pkgs")
err = os.MkdirAll(RepoDir, 0o755)
if err != nil {
log.Fatal("Unable to create repo cache directory").Err(err).Send()
}
err = os.MkdirAll(PkgsDir, 0o755)
if err != nil {
log.Fatal("Unable to create package cache directory").Err(err).Send()
}
DBPath = filepath.Join(CacheDir, "db")
fi, err := os.Stat(DBPath)
DBPresent = err == nil && !fi.IsDir()
}
var paths *Paths
func GetPaths() *Paths {
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

@ -26,16 +26,23 @@ import (
"golang.org/x/text/language"
)
var Language language.Tag
var (
lang language.Tag
langSet bool
)
func init() {
lang := SystemLang()
tag, err := language.Parse(lang)
if err != nil {
log.Fatal("Error parsing system language").Err(err).Send()
func Language() language.Tag {
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
}
base, _ := tag.Base()
Language = language.Make(base.String())
return lang
}
func SystemLang() string {

View File

@ -20,5 +20,7 @@ package config
import _ "embed"
//go:generate ../../scripts/gen-version.sh
//go:embed version.txt
var Version string

View File

@ -21,14 +21,15 @@ package cpu
import (
"os"
"runtime"
"strconv"
"strings"
"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") {
@ -44,34 +45,6 @@ func ARMVariant() string {
}
}
// CompatibleARM returns all the compatible ARM variants given the system architecture
func CompatibleARM(variant string) []string {
switch variant {
case "arm7", "arm":
return []string{"arm7", "arm6", "arm5"}
case "arm6":
return []string{"arm6", "arm5"}
case "arm5":
return []string{"arm5"}
default:
return []string{variant}
}
}
// CompatibleARMReverse returns all the compatible ARM variants given the package's architecture
func CompatibleARMReverse(variant string) []string {
switch variant {
case "arm7":
return []string{"arm7"}
case "arm6":
return []string{"arm6", "arm7"}
case "arm5", "arm":
return []string{"arm5", "arm6", "arm7"}
default:
return []string{variant}
}
}
// Arch returns the canonical CPU architecture of the system
func Arch() string {
arch := os.Getenv("LURE_ARCH")
@ -79,20 +52,65 @@ func Arch() string {
arch = runtime.GOARCH
}
if arch == "arm" {
arch = ARMVariant()
arch = armVariant()
}
return arch
}
// Arches returns all the architectures the system is compatible with
func Arches() []string {
arch := os.Getenv("LURE_ARCH")
if arch == "" {
arch = runtime.GOARCH
func IsCompatibleWith(target string, list []string) bool {
if target == "all" {
return true
}
if strings.HasPrefix(arch, "arm") {
return append(CompatibleARM(arch), "arm")
} else {
return []string{Arch()}
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)
}

View File

@ -62,40 +62,52 @@ type version struct {
Version int `db:"version"`
}
func Open(dsn string) (*sqlx.DB, error) {
if dsn != ":memory:" {
fi, err := os.Stat(config.DBPath)
if err == nil {
// TODO: This should be removed by the first stable release.
if fi.IsDir() {
log.Warn("Your database is using the old database engine; rebuilding").Send()
var (
conn *sqlx.DB
closed = true
)
err = os.RemoveAll(config.DBPath)
if err != nil {
log.Fatal("Error removing old database").Err(err).Send()
}
config.DBPresent = false
}
}
func DB() *sqlx.DB {
if conn != nil && !closed {
return conn
}
db, err := sqlx.Open("sqlite", dsn)
db, err := Open(config.GetPaths().DBPath)
if err != nil {
log.Fatal("Error opening database").Err(err).Send()
}
conn = db
return conn
}
err = Init(db, dsn)
func Open(dsn string) (*sqlx.DB, error) {
db, err := sqlx.Open("sqlite", dsn)
if err != nil {
log.Fatal("Error initializing database").Err(err).Send()
return nil, err
}
conn = db
closed = false
err = initDB(dsn)
if err != nil {
return nil, err
}
return db, nil
}
func Close() error {
closed = true
if conn != nil {
return conn.Close()
} else {
return nil
}
}
// Init initializes the database
func Init(db *sqlx.DB, dsn string) error {
*db = *db.Unsafe()
_, err := db.Exec(`
func initDB(dsn string) error {
conn = conn.Unsafe()
_, err := conn.Exec(`
CREATE TABLE IF NOT EXISTS pkgs (
name TEXT NOT NULL,
repository TEXT NOT NULL,
@ -123,49 +135,57 @@ func Init(db *sqlx.DB, dsn string) error {
return err
}
ver, ok := GetVersion(db)
ver, ok := GetVersion()
if !ok {
log.Warn("Database version does not exist. Run lure fix if something isn't working.").Send()
return addVersion(db, CurrentVersion)
return addVersion(CurrentVersion)
}
if ver != CurrentVersion {
log.Warn("Database version mismatch; rebuilding").Int("version", ver).Int("expected", CurrentVersion).Send()
db.Close()
err = os.Remove(config.DBPath)
conn.Close()
err = os.Remove(config.GetPaths().DBPath)
if err != nil {
return err
}
config.DBPresent = false
tdb, err := Open(dsn)
if err != nil {
return err
}
*db = *tdb
conn = tdb
}
return nil
}
func GetVersion(db *sqlx.DB) (int, bool) {
func IsEmpty() bool {
var count int
err := DB().Get(&count, "SELECT count(1) FROM pkgs;")
if err != nil {
return true
}
return count == 0
}
func GetVersion() (int, bool) {
var ver version
err := db.Get(&ver, "SELECT * FROM lure_db_version LIMIT 1;")
err := DB().Get(&ver, "SELECT * FROM lure_db_version LIMIT 1;")
if err != nil {
return 0, false
}
return ver.Version, true
}
func addVersion(db *sqlx.DB, ver int) error {
_, err := db.Exec(`INSERT INTO lure_db_version(version) VALUES (?);`, ver)
func addVersion(ver int) error {
_, err := DB().Exec(`INSERT INTO lure_db_version(version) VALUES (?);`, ver)
return err
}
// InsertPackage adds a package to the database
func InsertPackage(db *sqlx.DB, pkg Package) error {
_, err := db.NamedExec(`
func InsertPackage(pkg Package) error {
_, err := DB().NamedExec(`
INSERT OR REPLACE INTO pkgs (
name,
repository,
@ -204,8 +224,8 @@ func InsertPackage(db *sqlx.DB, pkg Package) error {
}
// GetPkgs returns a result containing packages that match the where conditions
func GetPkgs(db *sqlx.DB, where string, args ...any) (*sqlx.Rows, error) {
stream, err := db.Queryx("SELECT * FROM pkgs WHERE "+where, args...)
func GetPkgs(where string, args ...any) (*sqlx.Rows, error) {
stream, err := DB().Queryx("SELECT * FROM pkgs WHERE "+where, args...)
if err != nil {
return nil, err
}
@ -213,15 +233,15 @@ func GetPkgs(db *sqlx.DB, where string, args ...any) (*sqlx.Rows, error) {
}
// GetPkg returns a single package that match the where conditions
func GetPkg(db *sqlx.DB, where string, args ...any) (*Package, error) {
func GetPkg(where string, args ...any) (*Package, error) {
out := &Package{}
err := db.Get(out, "SELECT * FROM pkgs WHERE "+where+" LIMIT 1", args...)
err := DB().Get(out, "SELECT * FROM pkgs WHERE "+where+" LIMIT 1", args...)
return out, err
}
// DeletePkgs deletes all packages matching the where conditions
func DeletePkgs(db *sqlx.DB, where string, args ...any) error {
_, err := db.Exec("DELETE FROM pkgs WHERE "+where, args...)
func DeletePkgs(where string, args ...any) error {
_, err := DB().Exec("DELETE FROM pkgs WHERE "+where, args...)
return err
}

View File

@ -37,11 +37,11 @@ var testPkg = db.Package{
"ru": "Проверочный пакет",
}),
Homepage: db.NewJSON(map[string]string{
"en": "https://lure.arsenm.dev",
"en": "https://lure.elara.ws/",
}),
Maintainer: db.NewJSON(map[string]string{
"en": "Arsen Musayelyan <arsen@arsenm.dev>",
"ru": "Арсен Мусаелян <arsen@arsenm.dev>",
"en": "Elara Musayelyan <elara@elara.ws>",
"ru": "Элара Мусаелян <arsen@arsenm.dev>",
}),
Architectures: db.NewJSON([]string{"arm64", "amd64"}),
Licenses: db.NewJSON([]string{"GPL-3.0-or-later"}),
@ -59,23 +59,18 @@ var testPkg = db.Package{
}
func TestInit(t *testing.T) {
gdb, err := sqlx.Open("sqlite", ":memory:")
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer gdb.Close()
defer db.Close()
err = db.Init(gdb, ":memory:")
_, err = db.DB().Exec("SELECT * FROM pkgs")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
_, err = gdb.Exec("SELECT * FROM pkgs")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
ver, ok := db.GetVersion(gdb)
ver, ok := db.GetVersion()
if !ok {
t.Errorf("Expected version to be present")
} else if ver != db.CurrentVersion {
@ -84,19 +79,19 @@ func TestInit(t *testing.T) {
}
func TestInsertPackage(t *testing.T) {
gdb, err := db.Open(":memory:")
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer gdb.Close()
defer db.Close()
err = db.InsertPackage(gdb, testPkg)
err = db.InsertPackage(testPkg)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
dbPkg := db.Package{}
err = sqlx.Get(gdb, &dbPkg, "SELECT * FROM pkgs WHERE name = 'test' AND repository = 'default'")
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)
}
@ -107,28 +102,28 @@ func TestInsertPackage(t *testing.T) {
}
func TestGetPkgs(t *testing.T) {
gdb, err := db.Open(":memory:")
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer gdb.Close()
defer db.Close()
x1 := testPkg
x1.Name = "x1"
x2 := testPkg
x2.Name = "x2"
err = db.InsertPackage(gdb, x1)
err = db.InsertPackage(x1)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
err = db.InsertPackage(gdb, x2)
err = db.InsertPackage(x2)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
result, err := db.GetPkgs(gdb, "name LIKE 'x%'")
result, err := db.GetPkgs("name LIKE 'x%'")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
@ -147,28 +142,28 @@ func TestGetPkgs(t *testing.T) {
}
func TestGetPkg(t *testing.T) {
gdb, err := db.Open(":memory:")
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer gdb.Close()
defer db.Close()
x1 := testPkg
x1.Name = "x1"
x2 := testPkg
x2.Name = "x2"
err = db.InsertPackage(gdb, x1)
err = db.InsertPackage(x1)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
err = db.InsertPackage(gdb, x2)
err = db.InsertPackage(x2)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
pkg, err := db.GetPkg(gdb, "name LIKE 'x%' ORDER BY name")
pkg, err := db.GetPkg("name LIKE 'x%' ORDER BY name")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
@ -183,34 +178,34 @@ func TestGetPkg(t *testing.T) {
}
func TestDeletePkgs(t *testing.T) {
gdb, err := db.Open(":memory:")
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer gdb.Close()
defer db.Close()
x1 := testPkg
x1.Name = "x1"
x2 := testPkg
x2.Name = "x2"
err = db.InsertPackage(gdb, x1)
err = db.InsertPackage(x1)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
err = db.InsertPackage(gdb, x2)
err = db.InsertPackage(x2)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
err = db.DeletePkgs(gdb, "name = 'x1'")
err = db.DeletePkgs("name = 'x1'")
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
var dbPkg db.Package
err = gdb.Get(&dbPkg, "SELECT * FROM pkgs WHERE name LIKE 'x%' ORDER BY name LIMIT 1;")
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)
}
@ -221,11 +216,11 @@ func TestDeletePkgs(t *testing.T) {
}
func TestJsonArrayContains(t *testing.T) {
gdb, err := db.Open(":memory:")
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer gdb.Close()
defer db.Close()
x1 := testPkg
x1.Name = "x1"
@ -233,18 +228,18 @@ func TestJsonArrayContains(t *testing.T) {
x2.Name = "x2"
x2.Provides.Val = append(x2.Provides.Val, "x")
err = db.InsertPackage(gdb, x1)
err = db.InsertPackage(x1)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
err = db.InsertPackage(gdb, x2)
err = db.InsertPackage(x2)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
var dbPkg db.Package
err = gdb.Get(&dbPkg, "SELECT * FROM pkgs WHERE json_array_contains(provides, 'x');")
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)
}

View File

@ -28,8 +28,10 @@ import (
"go.elara.ws/lure/internal/config"
)
// BasePath stores the base path to the download cache
var BasePath = filepath.Join(config.CacheDir, "dl")
// BasePath returns the base path of the download cache
func BasePath() string {
return filepath.Join(config.GetPaths().RepoDir, "dl")
}
// New creates a new directory with the given ID in the cache.
// If a directory with the same ID already exists,
@ -39,7 +41,7 @@ func New(id string) (string, error) {
if err != nil {
return "", err
}
itemPath := filepath.Join(BasePath, h)
itemPath := filepath.Join(BasePath(), h)
fi, err := os.Stat(itemPath)
if err == nil || (fi != nil && !fi.IsDir()) {
@ -67,7 +69,7 @@ func Get(id string) (string, bool) {
if err != nil {
return "", false
}
itemPath := filepath.Join(BasePath, h)
itemPath := filepath.Join(BasePath(), h)
_, err = os.Stat(itemPath)
if err != nil {

View File

@ -26,6 +26,7 @@ import (
"path/filepath"
"testing"
"go.elara.ws/lure/internal/config"
"go.elara.ws/lure/internal/dlcache"
)
@ -34,7 +35,7 @@ func init() {
if err != nil {
panic(err)
}
dlcache.BasePath = dir
config.GetPaths().RepoDir = dir
}
func TestNew(t *testing.T) {
@ -44,7 +45,7 @@ func TestNew(t *testing.T) {
t.Errorf("Expected no error, got %s", err)
}
exp := filepath.Join(dlcache.BasePath, sha1sum(id))
exp := filepath.Join(dlcache.BasePath(), sha1sum(id))
if dir != exp {
t.Errorf("Expected %s, got %s", exp, dir)
}

View File

@ -58,7 +58,10 @@ func Resolve(info *distro.OSRelease, opts *Opts) ([]string, error) {
return nil, err
}
architectures := cpu.Arches()
architectures, err := cpu.CompatibleArches(cpu.Arch())
if err != nil {
return nil, err
}
distros := []string{info.ID}
if opts.LikeDistros {
@ -66,47 +69,38 @@ func Resolve(info *distro.OSRelease, opts *Opts) ([]string, error) {
}
var out []string
for _, arch := range architectures {
for _, lang := range langs {
for _, distro := range distros {
if opts.Name == "" {
out = append(
out,
arch+"_"+distro,
distro,
)
} else {
out = append(
out,
opts.Name+"_"+arch+"_"+distro,
opts.Name+"_"+distro,
)
for _, arch := range architectures {
out = append(out, opts.Name+"_"+arch+"_"+distro+"_"+lang)
}
out = append(out, opts.Name+"_"+distro+"_"+lang)
}
if opts.Name == "" {
out = append(out, arch)
} else {
out = append(out, opts.Name+"_"+arch)
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.ReplaceAll(item, "-", "_")
}
if len(langs) > 0 {
tmp := out
out = make([]string, 0, len(tmp)+(len(tmp)*len(langs)))
for _, lang := range langs {
for _, val := range tmp {
if val == "" {
continue
}
out = append(out, val+"_"+lang)
}
}
out = append(out, tmp...)
out[index] = strings.TrimPrefix(strings.ReplaceAll(item, "-", "_"), "_")
}
return out, nil

View File

@ -19,6 +19,7 @@
package overrides_test
import (
"os"
"reflect"
"testing"
@ -46,6 +47,7 @@ func TestResolve(t *testing.T) {
"amd64_fedora_en",
"fedora_en",
"amd64_en",
"en",
"amd64_centos",
"centos",
"amd64_rhel",
@ -87,6 +89,43 @@ func TestResolveName(t *testing.T) {
}
}
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,
@ -139,9 +178,11 @@ func TestResolveLangs(t *testing.T) {
"amd64_centos_en",
"centos_en",
"amd64_en",
"en",
"amd64_centos_ru",
"centos_ru",
"amd64_ru",
"ru",
"amd64_centos",
"centos",
"amd64",

View File

@ -34,13 +34,13 @@ var (
)
func init() {
b := lipgloss.RoundedBorder()
b.Right = "\u251C"
titleStyle = lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
b1 := lipgloss.RoundedBorder()
b1.Right = "\u251C"
titleStyle = lipgloss.NewStyle().BorderStyle(b1).Padding(0, 1)
b = lipgloss.RoundedBorder()
b.Left = "\u2524"
infoStyle = titleStyle.Copy().BorderStyle(b)
b2 := lipgloss.RoundedBorder()
b2.Left = "\u2524"
infoStyle = titleStyle.Copy().BorderStyle(b2)
}
type Pager struct {

View File

@ -18,15 +18,12 @@
package repos
import (
"github.com/jmoiron/sqlx"
"go.elara.ws/lure/internal/db"
)
import "go.elara.ws/lure/internal/db"
// FindPkgs looks for packages matching the inputs inside the database.
// It returns a map that maps the package name input to the packages found for it.
// It also returns a slice that contains the names of all packages that were not found.
func FindPkgs(gdb *sqlx.DB, pkgs []string) (map[string][]db.Package, []string, error) {
func FindPkgs(pkgs []string) (map[string][]db.Package, []string, error) {
found := map[string][]db.Package{}
notFound := []string(nil)
@ -35,7 +32,7 @@ func FindPkgs(gdb *sqlx.DB, pkgs []string) (map[string][]db.Package, []string, e
continue
}
result, err := db.GetPkgs(gdb, "name LIKE ?", pkgName)
result, err := db.GetPkgs("name LIKE ?", pkgName)
if err != nil {
return nil, nil, err
}
@ -54,7 +51,7 @@ func FindPkgs(gdb *sqlx.DB, pkgs []string) (map[string][]db.Package, []string, e
result.Close()
if added == 0 {
result, err := db.GetPkgs(gdb, "json_array_contains(provides, ?)", pkgName)
result, err := db.GetPkgs("json_array_contains(provides, ?)", pkgName)
if err != nil {
return nil, nil, err
}

View File

@ -30,18 +30,18 @@ import (
)
func TestFindPkgs(t *testing.T) {
gdb, err := db.Open(":memory:")
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer gdb.Close()
defer db.Close()
setCfgDirs(t)
defer removeCacheDir(t)
ctx := context.Background()
err = repos.Pull(ctx, gdb, []types.Repo{
err = repos.Pull(ctx, []types.Repo{
{
Name: "default",
URL: "https://github.com/Arsen6331/lure-repo.git",
@ -51,7 +51,7 @@ func TestFindPkgs(t *testing.T) {
t.Fatalf("Expected no error, got %s", err)
}
found, notFound, err := repos.FindPkgs(gdb, []string{"itd", "nonexistentpackage1", "nonexistentpackage2"})
found, notFound, err := repos.FindPkgs([]string{"itd", "nonexistentpackage1", "nonexistentpackage2"})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
@ -81,16 +81,16 @@ func TestFindPkgs(t *testing.T) {
}
func TestFindPkgsEmpty(t *testing.T) {
gdb, err := db.Open(":memory:")
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer gdb.Close()
defer db.Close()
setCfgDirs(t)
defer removeCacheDir(t)
err = db.InsertPackage(gdb, db.Package{
err = db.InsertPackage(db.Package{
Name: "test1",
Repository: "default",
Version: "0.0.1",
@ -105,7 +105,7 @@ func TestFindPkgsEmpty(t *testing.T) {
t.Fatalf("Expected no error, got %s", err)
}
err = db.InsertPackage(gdb, db.Package{
err = db.InsertPackage(db.Package{
Name: "test2",
Repository: "default",
Version: "0.0.1",
@ -120,7 +120,7 @@ func TestFindPkgsEmpty(t *testing.T) {
t.Fatalf("Expected no error, got %s", err)
}
found, notFound, err := repos.FindPkgs(gdb, []string{"test", ""})
found, notFound, err := repos.FindPkgs([]string{"test", ""})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}

View File

@ -33,7 +33,6 @@ import (
"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/jmoiron/sqlx"
"github.com/pelletier/go-toml/v2"
"go.elara.ws/logger/log"
"go.elara.ws/lure/distro"
@ -51,7 +50,7 @@ import (
// 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.
func Pull(ctx context.Context, gdb *sqlx.DB, repos []types.Repo) error {
func Pull(ctx context.Context, repos []types.Repo) error {
for _, repo := range repos {
repoURL, err := url.Parse(repo.URL)
if err != nil {
@ -59,7 +58,7 @@ func Pull(ctx context.Context, gdb *sqlx.DB, repos []types.Repo) error {
}
log.Info("Pulling repository").Str("name", repo.Name).Send()
repoDir := filepath.Join(config.RepoDir, repo.Name)
repoDir := filepath.Join(config.GetPaths().RepoDir, repo.Name)
var repoFS billy.Filesystem
gitDir := filepath.Join(repoDir, ".git")
@ -89,7 +88,7 @@ func Pull(ctx context.Context, gdb *sqlx.DB, repos []types.Repo) error {
repoFS = w.Filesystem
// Make sure the DB is created even if the repo is up to date
if !errors.Is(err, git.NoErrAlreadyUpToDate) || !config.DBPresent {
if !errors.Is(err, git.NoErrAlreadyUpToDate) || db.IsEmpty() {
new, err := r.Head()
if err != nil {
return err
@ -98,13 +97,13 @@ func Pull(ctx context.Context, gdb *sqlx.DB, repos []types.Repo) error {
// 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 config.DBPresent {
err = processRepoChanges(ctx, repo, r, w, old, new, gdb)
if db.IsEmpty() {
err = processRepoChanges(ctx, repo, r, w, old, new)
if err != nil {
return err
}
} else {
err = processRepoFull(ctx, repo, repoDir, gdb)
err = processRepoFull(ctx, repo, repoDir)
if err != nil {
return err
}
@ -129,7 +128,7 @@ func Pull(ctx context.Context, gdb *sqlx.DB, repos []types.Repo) error {
return err
}
err = processRepoFull(ctx, repo, repoDir, gdb)
err = processRepoFull(ctx, repo, repoDir)
if err != nil {
return err
}
@ -171,7 +170,7 @@ type action struct {
File string
}
func processRepoChanges(ctx context.Context, repo types.Repo, r *git.Repository, w *git.Worktree, old, new *plumbing.Reference, gdb *sqlx.DB) error {
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
@ -265,7 +264,7 @@ func processRepoChanges(ctx context.Context, repo types.Repo, r *git.Repository,
return err
}
err = db.DeletePkgs(gdb, "name = ? AND repository = ?", pkg.Name, repo.Name)
err = db.DeletePkgs("name = ? AND repository = ?", pkg.Name, repo.Name)
if err != nil {
return err
}
@ -300,7 +299,7 @@ func processRepoChanges(ctx context.Context, repo types.Repo, r *git.Repository,
resolveOverrides(runner, &pkg)
err = db.InsertPackage(gdb, pkg)
err = db.InsertPackage(pkg)
if err != nil {
return err
}
@ -326,7 +325,7 @@ func isValid(from, to diff.File) bool {
return match
}
func processRepoFull(ctx context.Context, repo types.Repo, repoDir string, gdb *sqlx.DB) error {
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 {
@ -370,7 +369,7 @@ func processRepoFull(ctx context.Context, repo types.Repo, repoDir string, gdb *
resolveOverrides(runner, &pkg)
err = db.InsertPackage(gdb, pkg)
err = db.InsertPackage(pkg)
if err != nil {
return err
}

View File

@ -33,50 +33,52 @@ import (
func setCfgDirs(t *testing.T) {
t.Helper()
paths := config.GetPaths()
var err error
config.CacheDir, err = os.MkdirTemp("/tmp", "lure-pull-test.*")
paths.CacheDir, err = os.MkdirTemp("/tmp", "lure-pull-test.*")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
config.RepoDir = filepath.Join(config.CacheDir, "repo")
config.PkgsDir = filepath.Join(config.CacheDir, "pkgs")
paths.RepoDir = filepath.Join(paths.CacheDir, "repo")
paths.PkgsDir = filepath.Join(paths.CacheDir, "pkgs")
err = os.MkdirAll(config.RepoDir, 0o755)
err = os.MkdirAll(paths.RepoDir, 0o755)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
err = os.MkdirAll(config.PkgsDir, 0o755)
err = os.MkdirAll(paths.PkgsDir, 0o755)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
config.DBPath = filepath.Join(config.CacheDir, "db")
paths.DBPath = filepath.Join(paths.CacheDir, "db")
}
func removeCacheDir(t *testing.T) {
t.Helper()
err := os.RemoveAll(config.CacheDir)
err := os.RemoveAll(config.GetPaths().CacheDir)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
}
func TestPull(t *testing.T) {
gdb, err := db.Open(":memory:")
_, err := db.Open(":memory:")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
defer gdb.Close()
defer db.Close()
setCfgDirs(t)
defer removeCacheDir(t)
ctx := context.Background()
err = repos.Pull(ctx, gdb, []types.Repo{
err = repos.Pull(ctx, []types.Repo{
{
Name: "default",
URL: "https://github.com/Arsen6331/lure-repo.git",
@ -86,7 +88,7 @@ func TestPull(t *testing.T) {
t.Fatalf("Expected no error, got %s", err)
}
result, err := db.GetPkgs(gdb, "name LIKE 'itd%'")
result, err := db.GetPkgs("name LIKE 'itd%'")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}

View File

@ -56,16 +56,16 @@ func (ite InvalidTypeError) Error() string {
// 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.
@ -173,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)
}
@ -188,7 +188,7 @@ func (d *Decoder) getFunc(name string) *syntax.Stmt {
}
for _, fnName := range names {
fn, ok := d.runner.Funcs[fnName]
fn, ok := d.Runner.Funcs[fnName]
if ok {
return fn
}
@ -205,11 +205,11 @@ func (d *Decoder) getVar(name string) *expand.Variable {
}
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 ""

View File

@ -36,8 +36,8 @@ const testScript = `
release=1
epoch=2
desc="Test package"
homepage='https://lure.arsenm.dev'
maintainer='Arsen Musayelyan <arsen@arsenm.dev>'
homepage='https://lure.elara.ws'
maintainer='Elara Musayelyan <elara@elara.ws>'
architectures=('arm64' 'amd64')
license=('GPL-3.0-or-later')
provides=('test')

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package helpers
import (
"errors"
@ -40,7 +40,8 @@ var (
ErrNoDetectManNum = errors.New("manual number cannot be detected from the filename")
)
var helpers = shutils.ExecFuncs{
// Helpers contains all the helper commands
var Helpers = shutils.ExecFuncs{
"install-binary": installHelperCmd("/usr/bin", 0o755),
"install-systemd-user": installHelperCmd("/usr/lib/systemd/user", 0o644),
"install-systemd": installHelperCmd("/usr/lib/systemd/system", 0o644),
@ -54,8 +55,9 @@ var helpers = shutils.ExecFuncs{
"git-version": gitVersionCmd,
}
// rHelpers contains restricted read-only helpers that don't modify any state
var rHelpers = shutils.ExecFuncs{
// Restricted contains restricted read-only helper commands
// that don't modify any state
var Restricted = shutils.ExecFuncs{
"git-version": gitVersionCmd,
}

View File

@ -21,7 +21,8 @@ package shutils
import (
"context"
"io"
"os"
"io/fs"
"path/filepath"
"strings"
"time"
@ -30,33 +31,36 @@ import (
)
func RestrictedReadDir(allowedPrefixes ...string) interp.ReadDirHandlerFunc {
return func(ctx context.Context, s string) ([]os.FileInfo, error) {
return func(ctx context.Context, s string) ([]fs.FileInfo, error) {
path := filepath.Clean(s)
for _, allowedPrefix := range allowedPrefixes {
if strings.HasPrefix(s, allowedPrefix) {
if strings.HasPrefix(path, allowedPrefix) {
return interp.DefaultReadDirHandler()(ctx, s)
}
}
return nil, os.ErrNotExist
return nil, fs.ErrNotExist
}
}
func RestrictedStat(allowedPrefixes ...string) interp.StatHandlerFunc {
return func(ctx context.Context, s string, b bool) (os.FileInfo, error) {
return func(ctx context.Context, s string, b bool) (fs.FileInfo, error) {
path := filepath.Clean(s)
for _, allowedPrefix := range allowedPrefixes {
if strings.HasPrefix(s, allowedPrefix) {
if strings.HasPrefix(path, allowedPrefix) {
return interp.DefaultStatHandler()(ctx, s, b)
}
}
return nil, os.ErrNotExist
return nil, fs.ErrNotExist
}
}
func RestrictedOpen(allowedPrefixes ...string) interp.OpenHandlerFunc {
return func(ctx context.Context, s string, i int, fm os.FileMode) (io.ReadWriteCloser, error) {
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(s, allowedPrefix) {
if strings.HasPrefix(path, allowedPrefix) {
return interp.DefaultOpenHandler()(ctx, s, i, fm)
}
}

View File

@ -0,0 +1,30 @@
package translations
import (
"embed"
"go.elara.ws/logger"
"go.elara.ws/logger/log"
"go.elara.ws/translate"
"golang.org/x/text/language"
)
//go:embed files
var translationFS embed.FS
var translator *translate.Translator
func Translator() *translate.Translator {
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(l logger.Logger, lang language.Tag) *translate.TranslatedLogger {
return translate.NewLogger(l, *Translator(), lang)
}

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

@ -0,0 +1,51 @@
package types
import "go.elara.ws/lure/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"`
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
}

110
list.go
View File

@ -23,71 +23,83 @@ import (
"github.com/urfave/cli/v2"
"go.elara.ws/logger/log"
"go.elara.ws/lure/internal/config"
"go.elara.ws/lure/internal/db"
"go.elara.ws/lure/internal/repos"
"go.elara.ws/lure/manager"
"golang.org/x/exp/slices"
)
func listCmd(c *cli.Context) error {
err := repos.Pull(c.Context, gdb, cfg.Repos)
if err != nil {
log.Fatal("Error pulling repositories").Err(err).Send()
}
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()}
}
result, err := db.GetPkgs(gdb, 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 supported package manager on system").Send()
}
installed, err = mgr.ListInstalled(&manager.Opts{AsRoot: false})
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 {
err := repos.Pull(c.Context, config.Config().Repos)
if err != nil {
log.Fatal("Error listing installed packages").Err(err).Send()
log.Fatal("Error pulling repositories").Err(err).Send()
}
}
for result.Next() {
var pkg db.Package
err := result.StructScan(&pkg)
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()}
}
result, err := db.GetPkgs(where, args...)
if err != nil {
return err
log.Fatal("Error getting packages").Err(err).Send()
}
defer result.Close()
if slices.Contains(cfg.IgnorePkgUpdates, pkg.Name) {
continue
}
version := pkg.Version
var installed map[string]string
if c.Bool("installed") {
instVersion, ok := installed[pkg.Name]
if !ok {
continue
} else {
version = instVersion
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()
}
}
fmt.Printf("%s/%s %s\n", pkg.Repository, pkg.Name, version)
}
for result.Next() {
var pkg db.Package
err := result.StructScan(&pkg)
if err != nil {
return err
}
if err != nil {
log.Fatal("Error iterating over packages").Err(err).Send()
}
if slices.Contains(config.Config().IgnorePkgUpdates, pkg.Name) {
continue
}
return nil
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
},
}

276
main.go
View File

@ -20,13 +20,11 @@ package main
import (
"context"
"embed"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
//"time"
"github.com/mattn/go-isatty"
"github.com/urfave/cli/v2"
@ -34,238 +32,78 @@ import (
"go.elara.ws/logger/log"
"go.elara.ws/lure/internal/config"
"go.elara.ws/lure/internal/db"
"go.elara.ws/lure/internal/translations"
"go.elara.ws/lure/manager"
"go.elara.ws/translate"
)
//go:generate scripts/gen-version.sh
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,
versionCmd,
},
Before: func(c *cli.Context) error {
args := strings.Split(c.String("pm-args"), " ")
if len(args) == 1 && args[0] == "" {
return nil
}
manager.Args = append(manager.Args, args...)
return nil
},
After: func(ctx *cli.Context) error {
return db.Close()
},
EnableBashCompletion: true,
}
//go:embed translations
var translationFS embed.FS
var translator translate.Translator
func init() {
logger := logger.NewCLI(os.Stderr)
t, err := translate.NewFromFS(translationFS)
if err != nil {
logger.Fatal("Error creating new translator").Err(err).Send()
}
translator = t
log.Logger = translate.NewLogger(logger, t, config.Language)
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() {
if !cfg.Unsafe.AllowRunAsRoot && os.Geteuid() == 0 {
log.Logger = translations.NewLogger(logger.NewCLI(os.Stderr), config.Language())
if !config.Config().Unsafe.AllowRunAsRoot && os.Geteuid() == 0 {
log.Fatal("Running LURE as root is forbidden as it may cause catastrophic damage to your system").Send()
}
err := loadDB()
if err != nil {
log.Fatal("Error loading database").Err(err).Send()
}
// Set the root command to the one set in the LURE config
manager.DefaultRootCmd = config.Config().RootCmd
ctx := context.Background()
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)
gdb.Close()
os.Exit(0)
}()
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{
{
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "clean",
Aliases: []string{"c"},
Usage: "Build package from scratch even if there's an already built package available",
},
},
Name: "install",
Usage: "Install a new package",
Aliases: []string{"in"},
Action: installCmd,
BashComplete: completionInstall,
},
{
Name: "remove",
Usage: "Remove an installed package",
Aliases: []string{"rm"},
Action: removeCmd,
},
{
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "clean",
Aliases: []string{"c"},
Usage: "Build package from scratch even if there's an already built package available",
},
},
Name: "upgrade",
Usage: "Upgrade all installed packages",
Aliases: []string{"up"},
Action: upgradeCmd,
},
{
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "all",
Aliases: []string{"a"},
Usage: "Show all information, not just for the current distro",
},
},
Name: "info",
Usage: "Print information about a package",
Action: infoCmd,
},
{
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "installed",
Aliases: []string{"I"},
},
},
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",
},
&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",
},
},
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: "fix",
Usage: "Attempt to fix problems with LURE",
Action: fixCmd,
},
{
Name: "version",
Usage: "Display the current LURE version and exit",
Action: displayVersion,
},
},
Before: func(c *cli.Context) error {
args := strings.Split(c.String("pm-args"), " ")
if len(args) == 1 && args[0] == "" {
args = nil
}
manager.Args = append(manager.Args, args...)
return nil
},
After: func(ctx *cli.Context) error {
return gdb.Close()
},
EnableBashCompletion: true,
}
err = app.RunContext(ctx, os.Args)
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(config.Version)
return nil
}
func completionInstall(c *cli.Context) {
result, err := db.GetPkgs(gdb, "true")
if err != nil {
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)
}
}

178
repo.go
View File

@ -32,83 +32,123 @@ import (
"golang.org/x/exp/slices"
)
func addrepoCmd(c *cli.Context) error {
name := c.String("name")
repoURL := c.String("url")
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 {
name := c.String("name")
repoURL := c.String("url")
for _, repo := range cfg.Repos {
if repo.URL == repoURL {
log.Fatal("Repo already exists").Str("name", repo.Name).Send()
cfg := config.Config()
for _, repo := range cfg.Repos {
if repo.URL == repoURL {
log.Fatal("Repo already exists").Str("name", repo.Name).Send()
}
}
}
cfg.Repos = append(cfg.Repos, types.Repo{
Name: name,
URL: repoURL,
})
cfg.Repos = append(cfg.Repos, types.Repo{
Name: name,
URL: repoURL,
})
cfgFl, err := os.Create(config.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 = repos.Pull(c.Context, gdb, cfg.Repos)
if err != nil {
log.Fatal("Error pulling repos").Err(err).Send()
}
return nil
}
func removerepoCmd(c *cli.Context) error {
name := c.String("name")
found := false
index := 0
for i, repo := range cfg.Repos {
if repo.Name == name {
index = i
found = true
cfgFl, err := os.Create(config.GetPaths().ConfigPath)
if err != nil {
log.Fatal("Error opening config file").Err(err).Send()
}
}
if !found {
log.Fatal("Repo does not exist").Str("name", name).Send()
}
cfg.Repos = slices.Delete(cfg.Repos, index, index+1)
err = toml.NewEncoder(cfgFl).Encode(cfg)
if err != nil {
log.Fatal("Error encoding config").Err(err).Send()
}
cfgFl, err := os.Create(config.ConfigPath)
if err != nil {
log.Fatal("Error opening config file").Err(err).Send()
}
err = repos.Pull(c.Context, cfg.Repos)
if err != nil {
log.Fatal("Error pulling repos").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.RepoDir, name))
if err != nil {
log.Fatal("Error removing repo directory").Err(err).Send()
}
err = db.DeletePkgs(gdb, "repository = ?", name)
if err != nil {
log.Fatal("Error removing packages from database").Err(err).Send()
}
return nil
return nil
},
}
func refreshCmd(c *cli.Context) error {
err := repos.Pull(c.Context, gdb, cfg.Repos)
if err != nil {
log.Fatal("Error pulling repos").Err(err).Send()
}
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 {
name := c.String("name")
cfg := config.Config()
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().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().RepoDir, name))
if err != nil {
log.Fatal("Error removing repo directory").Err(err).Send()
}
err = db.DeletePkgs("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 {
err := repos.Pull(c.Context, config.Config().Repos)
if err != nil {
log.Fatal("Error pulling repos").Err(err).Send()
}
return nil
},
}

View File

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

View File

@ -25,42 +25,61 @@ import (
"github.com/urfave/cli/v2"
"go.elara.ws/logger/log"
"go.elara.ws/lure/distro"
"go.elara.ws/lure/internal/build"
"go.elara.ws/lure/internal/config"
"go.elara.ws/lure/internal/db"
"go.elara.ws/lure/internal/repos"
"go.elara.ws/lure/internal/types"
"go.elara.ws/lure/manager"
"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 {
info, err := distro.ParseOSRelease(c.Context)
if err != nil {
log.Fatal("Error parsing os-release file").Err(err).Send()
}
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect supported package manager on system").Send()
}
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect a supported package manager on the system").Send()
}
err = repos.Pull(c.Context, gdb, cfg.Repos)
if err != nil {
log.Fatal("Error pulling repos").Err(err).Send()
}
err = repos.Pull(c.Context, config.Config().Repos)
if err != nil {
log.Fatal("Error pulling repos").Err(err).Send()
}
updates, err := checkForUpdates(c.Context, mgr, info)
if err != nil {
log.Fatal("Error checking for updates").Err(err).Send()
}
updates, err := checkForUpdates(c.Context, mgr, info)
if err != nil {
log.Fatal("Error checking for updates").Err(err).Send()
}
if len(updates) > 0 {
installPkgs(c.Context, updates, nil, mgr, c.Bool("clean"), c.Bool("interactive"))
} else {
log.Info("There is nothing to do.").Send()
}
if len(updates) > 0 {
build.InstallPkgs(c.Context, 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
return nil
},
}
func checkForUpdates(ctx context.Context, mgr manager.Manager, info *distro.OSRelease) ([]db.Package, error) {
@ -70,14 +89,14 @@ func checkForUpdates(ctx context.Context, mgr manager.Manager, info *distro.OSRe
}
pkgNames := maps.Keys(installed)
found, _, err := repos.FindPkgs(gdb, pkgNames)
found, _, err := repos.FindPkgs(pkgNames)
if err != nil {
return nil, err
}
var out []db.Package
for pkgName, pkgs := range found {
if slices.Contains(cfg.IgnorePkgUpdates, pkgName) {
if slices.Contains(config.Config().IgnorePkgUpdates, pkgName) {
continue
}