From 45522e3f3a89290247e35d34b194349933398dc3 Mon Sep 17 00:00:00 2001 From: Elara Musayelyan Date: Tue, 19 Sep 2023 14:28:05 -0700 Subject: [PATCH] Major refactor --- Makefile | 2 +- build.go | 760 ++---------------- cmd/lure-api-server/api.go | 11 +- cmd/lure-api-server/badge.go | 5 +- cmd/lure-api-server/config.go | 34 - cmd/lure-api-server/db.go | 36 - cmd/lure-api-server/main.go | 7 +- cmd/lure-api-server/webhook.go | 3 +- config.go | 36 - db.go | 36 - distro/osrelease.go | 9 +- fix.go | 48 +- info.go | 105 +-- install.go | 154 ++-- internal/api/lure.pb.go | 2 +- internal/build/build.go | 728 +++++++++++++++++ internal/build/install.go | 51 ++ internal/cliutils/prompt.go | 23 +- internal/config/config.go | 41 +- internal/config/dirs.go | 118 +-- internal/config/lang.go | 23 +- internal/config/version.go | 2 + internal/cpu/cpu.go | 98 ++- internal/db/db.go | 98 ++- internal/db/db_test.go | 69 +- internal/dlcache/dlcache.go | 10 +- internal/dlcache/dlcache_test.go | 5 +- internal/overrides/overrides.go | 62 +- internal/overrides/overrides_test.go | 41 + internal/pager/pager.go | 12 +- internal/repos/find.go | 11 +- internal/repos/find_test.go | 18 +- internal/repos/pull.go | 25 +- internal/repos/pull_test.go | 24 +- internal/shutils/decoder/decoder.go | 14 +- internal/shutils/exec_test.go | 4 +- .../shutils/helpers/helpers.go | 10 +- internal/shutils/restricted.go | 22 +- .../translations/files}/lure.en.toml | 0 .../translations/files}/lure.ru.toml | 0 internal/translations/translations.go | 30 + internal/types/build.go | 51 ++ list.go | 110 +-- main.go | 276 ++----- repo.go | 178 ++-- scripts/gen-version.sh | 2 +- upgrade.go | 69 +- 47 files changed, 1780 insertions(+), 1693 deletions(-) delete mode 100644 cmd/lure-api-server/config.go delete mode 100644 cmd/lure-api-server/db.go delete mode 100644 config.go delete mode 100644 db.go create mode 100644 internal/build/build.go create mode 100644 internal/build/install.go rename helpers.go => internal/shutils/helpers/helpers.go (97%) rename {translations => internal/translations/files}/lure.en.toml (100%) rename {translations => internal/translations/files}/lure.ru.toml (100%) create mode 100644 internal/translations/translations.go create mode 100644 internal/types/build.go diff --git a/Makefile b/Makefile index b42025d..8148dd9 100644 --- a/Makefile +++ b/Makefile @@ -19,4 +19,4 @@ uninstall: internal/config/version.txt: go generate ./internal/config -.PHONY: install clean uninstall \ No newline at end of file +.PHONY: install clean uninstall installmisc \ No newline at end of file diff --git a/build.go b/build.go index bc48647..5b989e8 100644 --- a/build.go +++ b/build.go @@ -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) - } + }, } diff --git a/cmd/lure-api-server/api.go b/cmd/lure-api-server/api.go index b7934e2..cdd3fdf 100644 --- a/cmd/lure-api-server/api.go +++ b/cmd/lure-api-server/api.go @@ -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") diff --git a/cmd/lure-api-server/badge.go b/cmd/lure-api-server/badge.go index 5c764bd..aa08761 100644 --- a/cmd/lure-api-server/badge.go +++ b/cmd/lure-api-server/badge.go @@ -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 diff --git a/cmd/lure-api-server/config.go b/cmd/lure-api-server/config.go deleted file mode 100644 index 255e23b..0000000 --- a/cmd/lure-api-server/config.go +++ /dev/null @@ -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 . - */ - -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() - } -} diff --git a/cmd/lure-api-server/db.go b/cmd/lure-api-server/db.go deleted file mode 100644 index c3ab12e..0000000 --- a/cmd/lure-api-server/db.go +++ /dev/null @@ -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 . - */ - -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() - } -} diff --git a/cmd/lure-api-server/main.go b/cmd/lure-api-server/main.go index be7e09b..de6e1e4 100644 --- a/cmd/lure-api-server/main.go +++ b/cmd/lure-api-server/main.go @@ -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 { diff --git a/cmd/lure-api-server/webhook.go b/cmd/lure-api-server/webhook.go index bfc3cf8..b7ce1b9 100644 --- a/cmd/lure-api-server/webhook.go +++ b/cmd/lure-api-server/webhook.go @@ -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() } diff --git a/config.go b/config.go deleted file mode 100644 index 84a99b8..0000000 --- a/config.go +++ /dev/null @@ -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 . - */ - -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 -} diff --git a/db.go b/db.go deleted file mode 100644 index 4022524..0000000 --- a/db.go +++ /dev/null @@ -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 . - */ - -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 -} diff --git a/distro/osrelease.go b/distro/osrelease.go index 6908435..5149a0a 100644 --- a/distro/osrelease.go +++ b/distro/osrelease.go @@ -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, " ") } diff --git a/fix.go b/fix.go index 67009ea..eec332c 100644 --- a/fix.go +++ b/fix.go @@ -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 + }, } diff --git a/info.go b/info.go index 4b44cac..b983868 100644 --- a/info.go +++ b/info.go @@ -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 + }, } diff --git a/install.go b/install.go index 2fa21b6..79db45a 100644 --- a/install.go +++ b/install.go @@ -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 + }, } diff --git a/internal/api/lure.pb.go b/internal/api/lure.pb.go index eb3b23a..7dfd48f 100644 --- a/internal/api/lure.pb.go +++ b/internal/api/lure.pb.go @@ -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 diff --git a/internal/build/build.go b/internal/build/build.go new file mode 100644 index 0000000..a7f7721 --- /dev/null +++ b/internal/build/build.go @@ -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 +} diff --git a/internal/build/install.go b/internal/build/install.go new file mode 100644 index 0000000..c0ab693 --- /dev/null +++ b/internal/build/install.go @@ -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() + } + } +} diff --git a/internal/cliutils/prompt.go b/internal/cliutils/prompt.go index f95a741..86d5efc 100644 --- a/internal/cliutils/prompt.go +++ b/internal/cliutils/prompt.go @@ -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 diff --git a/internal/config/config.go b/internal/config/config.go index 8d1c54e..0350446 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 } diff --git a/internal/config/dirs.go b/internal/config/dirs.go index 9785af0..90844d1 100644 --- a/internal/config/dirs.go +++ b/internal/config/dirs.go @@ -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 } diff --git a/internal/config/lang.go b/internal/config/lang.go index 483f0f6..72490cb 100644 --- a/internal/config/lang.go +++ b/internal/config/lang.go @@ -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 { diff --git a/internal/config/version.go b/internal/config/version.go index 37fb029..118804a 100644 --- a/internal/config/version.go +++ b/internal/config/version.go @@ -20,5 +20,7 @@ package config import _ "embed" +//go:generate ../../scripts/gen-version.sh + //go:embed version.txt var Version string diff --git a/internal/cpu/cpu.go b/internal/cpu/cpu.go index ff3f349..8204fec 100644 --- a/internal/cpu/cpu.go +++ b/internal/cpu/cpu.go @@ -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) } diff --git a/internal/db/db.go b/internal/db/db.go index 74d0016..8c977af 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -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 } diff --git a/internal/db/db_test.go b/internal/db/db_test.go index 272b9ad..96b3129 100644 --- a/internal/db/db_test.go +++ b/internal/db/db_test.go @@ -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 ", - "ru": "Арсен Мусаелян ", + "en": "Elara Musayelyan ", + "ru": "Элара Мусаелян ", }), 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) } diff --git a/internal/dlcache/dlcache.go b/internal/dlcache/dlcache.go index c5f9b93..bad2089 100644 --- a/internal/dlcache/dlcache.go +++ b/internal/dlcache/dlcache.go @@ -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 { diff --git a/internal/dlcache/dlcache_test.go b/internal/dlcache/dlcache_test.go index 4c0b923..986e9d4 100644 --- a/internal/dlcache/dlcache_test.go +++ b/internal/dlcache/dlcache_test.go @@ -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) } diff --git a/internal/overrides/overrides.go b/internal/overrides/overrides.go index 3e6c741..d8b915b 100644 --- a/internal/overrides/overrides.go +++ b/internal/overrides/overrides.go @@ -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 diff --git a/internal/overrides/overrides_test.go b/internal/overrides/overrides_test.go index 2927944..99b886c 100644 --- a/internal/overrides/overrides_test.go +++ b/internal/overrides/overrides_test.go @@ -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", diff --git a/internal/pager/pager.go b/internal/pager/pager.go index fe34ead..d1d91bb 100644 --- a/internal/pager/pager.go +++ b/internal/pager/pager.go @@ -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 { diff --git a/internal/repos/find.go b/internal/repos/find.go index 221bc39..1e9310d 100644 --- a/internal/repos/find.go +++ b/internal/repos/find.go @@ -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 } diff --git a/internal/repos/find_test.go b/internal/repos/find_test.go index f88ebeb..35d40fe 100644 --- a/internal/repos/find_test.go +++ b/internal/repos/find_test.go @@ -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) } diff --git a/internal/repos/pull.go b/internal/repos/pull.go index d9c52cf..fe3e45b 100644 --- a/internal/repos/pull.go +++ b/internal/repos/pull.go @@ -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 } diff --git a/internal/repos/pull_test.go b/internal/repos/pull_test.go index d64983a..0c72a1c 100644 --- a/internal/repos/pull_test.go +++ b/internal/repos/pull_test.go @@ -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) } diff --git a/internal/shutils/decoder/decoder.go b/internal/shutils/decoder/decoder.go index 2c8164b..4f308c4 100644 --- a/internal/shutils/decoder/decoder.go +++ b/internal/shutils/decoder/decoder.go @@ -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 "" diff --git a/internal/shutils/exec_test.go b/internal/shutils/exec_test.go index be14a47..c3287e0 100644 --- a/internal/shutils/exec_test.go +++ b/internal/shutils/exec_test.go @@ -36,8 +36,8 @@ const testScript = ` release=1 epoch=2 desc="Test package" - homepage='https://lure.arsenm.dev' - maintainer='Arsen Musayelyan ' + homepage='https://lure.elara.ws' + maintainer='Elara Musayelyan ' architectures=('arm64' 'amd64') license=('GPL-3.0-or-later') provides=('test') diff --git a/helpers.go b/internal/shutils/helpers/helpers.go similarity index 97% rename from helpers.go rename to internal/shutils/helpers/helpers.go index 10aac3b..f43547e 100644 --- a/helpers.go +++ b/internal/shutils/helpers/helpers.go @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -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, } diff --git a/internal/shutils/restricted.go b/internal/shutils/restricted.go index e21e54e..e9325bb 100644 --- a/internal/shutils/restricted.go +++ b/internal/shutils/restricted.go @@ -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) } } diff --git a/translations/lure.en.toml b/internal/translations/files/lure.en.toml similarity index 100% rename from translations/lure.en.toml rename to internal/translations/files/lure.en.toml diff --git a/translations/lure.ru.toml b/internal/translations/files/lure.ru.toml similarity index 100% rename from translations/lure.ru.toml rename to internal/translations/files/lure.ru.toml diff --git a/internal/translations/translations.go b/internal/translations/translations.go new file mode 100644 index 0000000..fb10ad4 --- /dev/null +++ b/internal/translations/translations.go @@ -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) +} diff --git a/internal/types/build.go b/internal/types/build.go new file mode 100644 index 0000000..6f1e226 --- /dev/null +++ b/internal/types/build.go @@ -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 +} diff --git a/list.go b/list.go index 24249a8..bcd0f1d 100644 --- a/list.go +++ b/list.go @@ -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 + }, } diff --git a/main.go b/main.go index ce05db8..fce92c0 100644 --- a/main.go +++ b/main.go @@ -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) - } -} diff --git a/repo.go b/repo.go index 80a94ee..cdf8cb5 100644 --- a/repo.go +++ b/repo.go @@ -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 + }, } diff --git a/scripts/gen-version.sh b/scripts/gen-version.sh index 491f6de..9eb213e 100755 --- a/scripts/gen-version.sh +++ b/scripts/gen-version.sh @@ -1,3 +1,3 @@ #!/bin/bash -git describe --tags > internal/config/version.txt \ No newline at end of file +git describe --tags > version.txt \ No newline at end of file diff --git a/upgrade.go b/upgrade.go index fa6e512..0d7f0e7 100644 --- a/upgrade.go +++ b/upgrade.go @@ -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 }