2026-03-25 14:41:05 +03:00
|
|
|
package pkg
|
|
|
|
|
|
|
|
|
|
import (
|
2026-08-11 18:58:45 +03:00
|
|
|
"archive/tar"
|
2026-03-25 14:41:05 +03:00
|
|
|
"bufio"
|
2026-08-11 18:58:45 +03:00
|
|
|
"context"
|
2026-05-05 23:25:59 +03:00
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2026-08-11 18:58:45 +03:00
|
|
|
"io"
|
|
|
|
|
"net/http"
|
2026-03-25 14:41:05 +03:00
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
2026-08-11 18:58:45 +03:00
|
|
|
"regexp"
|
|
|
|
|
"runtime"
|
|
|
|
|
"slices"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
2026-05-05 23:25:59 +03:00
|
|
|
paths "qulay/config"
|
2026-03-25 14:41:05 +03:00
|
|
|
"qulay/database"
|
|
|
|
|
"qulay/helpers"
|
2026-05-05 23:25:59 +03:00
|
|
|
|
2026-03-25 14:41:05 +03:00
|
|
|
"codeberg.org/UzbekLinux/uzbekdb-go"
|
2026-08-11 18:58:45 +03:00
|
|
|
"github.com/codeclysm/extract/v3"
|
|
|
|
|
"github.com/klauspost/compress/zstd"
|
2026-03-25 14:41:05 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var _name string = filepath.Base(os.Args[0])
|
|
|
|
|
|
2026-07-15 14:33:08 +03:00
|
|
|
var installing = map[string]bool{}
|
|
|
|
|
|
2026-03-25 14:41:05 +03:00
|
|
|
type Package struct {
|
2026-08-11 18:58:45 +03:00
|
|
|
Name string
|
|
|
|
|
Repo string
|
|
|
|
|
Version string
|
|
|
|
|
Desc string
|
|
|
|
|
|
|
|
|
|
Depends []string
|
|
|
|
|
Type string
|
|
|
|
|
URL string
|
|
|
|
|
|
|
|
|
|
Conflicts []string
|
|
|
|
|
|
|
|
|
|
After string
|
|
|
|
|
Remove string
|
2026-03-25 14:41:05 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
Message string
|
|
|
|
|
|
|
|
|
|
Path string
|
2026-03-25 14:41:05 +03:00
|
|
|
}
|
|
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
func readPackage(repo string, path string, repoURL string) (*Package, error) {
|
|
|
|
|
f, err := os.Open(filepath.Join(path, "manifest"))
|
2026-03-25 14:41:05 +03:00
|
|
|
if err != nil {
|
2026-08-11 18:58:45 +03:00
|
|
|
return nil, err
|
2026-03-25 14:41:05 +03:00
|
|
|
}
|
|
|
|
|
defer f.Close()
|
2026-08-11 18:58:45 +03:00
|
|
|
|
|
|
|
|
pkg := &Package{
|
|
|
|
|
Repo: repo,
|
|
|
|
|
Path: path,
|
|
|
|
|
}
|
2026-07-15 14:33:08 +03:00
|
|
|
|
2026-03-25 14:41:05 +03:00
|
|
|
scanner := bufio.NewScanner(f)
|
2026-08-11 18:58:45 +03:00
|
|
|
|
2026-03-25 14:41:05 +03:00
|
|
|
for scanner.Scan() {
|
2026-08-11 18:58:45 +03:00
|
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
|
|
|
|
|
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch {
|
|
|
|
|
case strings.HasPrefix(line, "name:"):
|
|
|
|
|
pkg.Name = strings.TrimSpace(strings.TrimPrefix(line, "name:"))
|
|
|
|
|
|
|
|
|
|
case strings.HasPrefix(line, "repo:"):
|
|
|
|
|
if repo == "" {
|
|
|
|
|
repo = strings.TrimSpace(strings.TrimPrefix(line, "repo:"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case strings.HasPrefix(line, "ver:"):
|
|
|
|
|
pkg.Version = strings.TrimSpace(strings.TrimPrefix(line, "ver:"))
|
|
|
|
|
|
|
|
|
|
case strings.HasPrefix(line, "desc:"):
|
|
|
|
|
pkg.Desc = strings.TrimSpace(strings.TrimPrefix(line, "desc:"))
|
|
|
|
|
|
|
|
|
|
case strings.HasPrefix(line, "deps:"):
|
|
|
|
|
pkg.Depends = append(pkg.Depends,
|
|
|
|
|
strings.Fields(strings.TrimSpace(strings.TrimPrefix(line, "deps:")))...)
|
|
|
|
|
|
|
|
|
|
case strings.HasPrefix(line, "conflicts:"):
|
|
|
|
|
pkg.Conflicts = append(pkg.Conflicts,
|
|
|
|
|
strings.Fields(strings.TrimSpace(strings.TrimPrefix(line, "conflicts:")))...)
|
|
|
|
|
|
|
|
|
|
case strings.HasPrefix(line, "type:"):
|
|
|
|
|
pkg.Type = strings.TrimSpace(strings.TrimPrefix(line, "type:"))
|
|
|
|
|
|
|
|
|
|
case strings.HasPrefix(line, "url:"):
|
|
|
|
|
pkg.URL = strings.TrimSpace(strings.TrimPrefix(line, "url:"))
|
|
|
|
|
|
|
|
|
|
case strings.HasPrefix(line, "after:"):
|
|
|
|
|
pkg.After = strings.TrimSpace(strings.TrimPrefix(line, "after:"))
|
|
|
|
|
|
|
|
|
|
case strings.HasPrefix(line, "remove:"):
|
|
|
|
|
pkg.Remove = strings.TrimSpace(strings.TrimPrefix(line, "remove:"))
|
|
|
|
|
|
|
|
|
|
case strings.HasPrefix(line, "message:"):
|
|
|
|
|
pkg.Message = strings.TrimSpace(strings.TrimPrefix(line, "message:"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pkg.Repo = repo
|
|
|
|
|
|
|
|
|
|
subst := func(s string) string {
|
|
|
|
|
if repoURL != "" {
|
|
|
|
|
s = strings.ReplaceAll(s, "{URL}", repoURL)
|
|
|
|
|
}
|
|
|
|
|
s = strings.ReplaceAll(s, "{ARCH}", runtime.GOARCH)
|
|
|
|
|
s = strings.ReplaceAll(s, "{REPO}", pkg.Repo)
|
|
|
|
|
s = strings.ReplaceAll(s, "{NAME}", pkg.Name)
|
|
|
|
|
s = strings.ReplaceAll(s, "{VER}", pkg.Version)
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pkg.Name = subst(pkg.Name)
|
|
|
|
|
pkg.Version = subst(pkg.Version)
|
|
|
|
|
pkg.Desc = subst(pkg.Desc)
|
|
|
|
|
pkg.URL = subst(pkg.URL)
|
|
|
|
|
pkg.Type = subst(pkg.Type)
|
|
|
|
|
pkg.After = subst(pkg.After)
|
|
|
|
|
pkg.Remove = subst(pkg.Remove)
|
|
|
|
|
pkg.Message = subst(pkg.Message)
|
|
|
|
|
for i, dep := range pkg.Depends {
|
|
|
|
|
pkg.Depends[i] = subst(dep)
|
|
|
|
|
}
|
|
|
|
|
for i, conflict := range pkg.Conflicts {
|
|
|
|
|
pkg.Conflicts[i] = subst(conflict)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pkg, nil
|
2026-03-25 14:41:05 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func GetPackages(destDir string) []Package {
|
2026-05-05 23:25:59 +03:00
|
|
|
var found []Package
|
2026-08-11 18:58:45 +03:00
|
|
|
|
|
|
|
|
reposPath := filepath.Join(destDir, paths.ReposDir)
|
|
|
|
|
|
|
|
|
|
repoURLs := map[string]string{}
|
|
|
|
|
for _, repo := range database.GetRepos(destDir) {
|
|
|
|
|
repoURLs[repo.Name] = repo.URL
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
repos, err := os.ReadDir(reposPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return found
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, repo := range repos {
|
|
|
|
|
repoPath := filepath.Join(
|
|
|
|
|
reposPath,
|
|
|
|
|
repo.Name(),
|
|
|
|
|
)
|
|
|
|
|
repoInfo, err := os.Stat(repoPath)
|
|
|
|
|
if err != nil || !repoInfo.IsDir() {
|
2026-05-05 23:25:59 +03:00
|
|
|
continue
|
|
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
|
|
|
|
|
packagesPath := filepath.Join(
|
|
|
|
|
repoPath,
|
|
|
|
|
"packages",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
packages, err := os.ReadDir(packagesPath)
|
2026-05-05 23:25:59 +03:00
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
|
|
|
|
|
for _, pkgDir := range packages {
|
|
|
|
|
pkgPath := filepath.Join(
|
|
|
|
|
packagesPath,
|
|
|
|
|
pkgDir.Name(),
|
|
|
|
|
)
|
|
|
|
|
pkgInfo, err := os.Stat(pkgPath)
|
|
|
|
|
if err != nil || !pkgInfo.IsDir() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pkg, err := readPackage(repo.Name(), pkgPath, repoURLs[repo.Name()])
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if pkg.Name == "" {
|
|
|
|
|
pkg.Name = pkgDir.Name()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
found = append(found, *pkg)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slices.SortFunc(found, func(a, b Package) int {
|
|
|
|
|
if n := strings.Compare(a.Repo, b.Repo); n != 0 {
|
|
|
|
|
return n
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return strings.Compare(a.Name, b.Name)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return found
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func IsBuilt(pkg *Package, destDir string) bool {
|
|
|
|
|
builtDir := filepath.Join(
|
|
|
|
|
destDir,
|
|
|
|
|
paths.BuiltDir,
|
|
|
|
|
runtime.GOARCH,
|
|
|
|
|
pkg.Repo,
|
|
|
|
|
)
|
|
|
|
|
if _, err := os.Stat(
|
|
|
|
|
filepath.Join(builtDir, pkg.Name+".tar.zst"),
|
|
|
|
|
); err == nil {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if _, err := os.Stat(
|
|
|
|
|
filepath.Join(builtDir, pkg.Name+".qpmp"),
|
|
|
|
|
); err == nil {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var versionRe = regexp.MustCompile(`[0-9]+|[a-zA-Z]+`)
|
|
|
|
|
|
|
|
|
|
func trimTrailingZeros(segs []string) []string {
|
|
|
|
|
for len(segs) > 0 {
|
|
|
|
|
last := segs[len(segs)-1]
|
|
|
|
|
if n, err := strconv.Atoi(last); err == nil && n == 0 {
|
|
|
|
|
segs = segs[:len(segs)-1]
|
|
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return segs
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func versionCompare(a, b string) int {
|
|
|
|
|
aa := trimTrailingZeros(versionRe.FindAllString(a, -1))
|
|
|
|
|
bb := trimTrailingZeros(versionRe.FindAllString(b, -1))
|
|
|
|
|
|
|
|
|
|
n := len(aa)
|
|
|
|
|
if len(bb) < n {
|
|
|
|
|
n = len(bb)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i := 0; i < n; i++ {
|
|
|
|
|
x, y := aa[i], bb[i]
|
|
|
|
|
|
|
|
|
|
xn, xerr := strconv.Atoi(x)
|
|
|
|
|
yn, yerr := strconv.Atoi(y)
|
|
|
|
|
if xerr == nil && yerr == nil {
|
|
|
|
|
if xn != yn {
|
|
|
|
|
if xn > yn {
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if x != y {
|
|
|
|
|
if x > y {
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(aa) < len(bb) {
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
if len(aa) > len(bb) {
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isStale(pkg *Package, destDir string) bool {
|
|
|
|
|
builtDir := filepath.Join(
|
|
|
|
|
destDir,
|
|
|
|
|
paths.BuiltDir,
|
|
|
|
|
runtime.GOARCH,
|
|
|
|
|
pkg.Repo,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
artifact := filepath.Join(builtDir, pkg.Name+".tar.zst")
|
|
|
|
|
if _, err := os.Stat(artifact); err != nil {
|
|
|
|
|
artifact = filepath.Join(builtDir, pkg.Name+".qpmp")
|
|
|
|
|
if _, err := os.Stat(artifact); err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
artInfo, err := os.Stat(artifact)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, src := range []string{"manifest", "build.sh"} {
|
|
|
|
|
info, err := os.Stat(filepath.Join(pkg.Path, src))
|
|
|
|
|
if err == nil && info.ModTime().After(artInfo.ModTime()) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isOldInstalled(key string, destDir string) bool {
|
|
|
|
|
pkg, err := getPackage(key, destDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
ver := database.GetPackageVersion(key, destDir)
|
|
|
|
|
return ver != "" && versionCompare(pkg.Version, ver) > 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func buildable(p *Package) bool {
|
|
|
|
|
return p.Type == "archive" || p.Type == "binary"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func Expand(cmd string, oths []string, destDir string) ([]string, error) {
|
|
|
|
|
if cmd == "o" && len(oths) == 0 {
|
|
|
|
|
oths = []string{"@old"}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var out []string
|
|
|
|
|
seen := map[string]bool{}
|
|
|
|
|
|
|
|
|
|
add := func(name string) {
|
|
|
|
|
if !seen[name] {
|
|
|
|
|
seen[name] = true
|
|
|
|
|
out = append(out, name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, arg := range oths {
|
|
|
|
|
lower := strings.ToLower(arg)
|
|
|
|
|
|
|
|
|
|
var repo, kind string
|
|
|
|
|
if strings.Contains(lower, "/") {
|
|
|
|
|
parts := strings.SplitN(lower, "/", 2)
|
|
|
|
|
repo = parts[0]
|
|
|
|
|
kind = parts[1]
|
|
|
|
|
} else {
|
|
|
|
|
kind = lower
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch kind {
|
|
|
|
|
case "@all", "@inst", "@notinst", "@old":
|
|
|
|
|
default:
|
|
|
|
|
add(arg)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
packages := GetPackages(destDir)
|
|
|
|
|
|
|
|
|
|
switch cmd {
|
|
|
|
|
case "-":
|
|
|
|
|
for key := range database.ReadInstalled(destDir) {
|
|
|
|
|
ks, ok := key.(string)
|
|
|
|
|
if !ok || (repo != "" && !strings.HasPrefix(ks, repo+"/")) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch kind {
|
|
|
|
|
case "@all", "@inst":
|
|
|
|
|
add(ks)
|
|
|
|
|
case "@old":
|
|
|
|
|
if isOldInstalled(ks, destDir) {
|
|
|
|
|
add(ks)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "b":
|
|
|
|
|
for i := range packages {
|
|
|
|
|
p := &packages[i]
|
|
|
|
|
if !buildable(p) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if repo != "" && strings.ToLower(p.Repo) != repo {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
key := p.Repo + "/" + p.Name
|
|
|
|
|
|
|
|
|
|
switch kind {
|
|
|
|
|
case "@all":
|
|
|
|
|
add(key)
|
|
|
|
|
case "@inst":
|
|
|
|
|
if IsBuilt(p, destDir) {
|
|
|
|
|
add(key)
|
|
|
|
|
}
|
|
|
|
|
case "@notinst":
|
|
|
|
|
if !IsBuilt(p, destDir) {
|
|
|
|
|
add(key)
|
|
|
|
|
}
|
|
|
|
|
case "@old":
|
|
|
|
|
if IsBuilt(p, destDir) && isStale(p, destDir) {
|
|
|
|
|
add(key)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default: // "+" and "o"
|
|
|
|
|
for i := range packages {
|
|
|
|
|
p := &packages[i]
|
|
|
|
|
if repo != "" && strings.ToLower(p.Repo) != repo {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
key := p.Repo + "/" + p.Name
|
|
|
|
|
installed := database.IsInstalled(key, destDir)
|
|
|
|
|
|
|
|
|
|
switch kind {
|
|
|
|
|
case "@all":
|
|
|
|
|
add(key)
|
|
|
|
|
case "@inst":
|
|
|
|
|
if installed {
|
|
|
|
|
add(key)
|
|
|
|
|
}
|
|
|
|
|
case "@notinst":
|
|
|
|
|
if !installed {
|
|
|
|
|
add(key)
|
|
|
|
|
}
|
|
|
|
|
case "@old":
|
|
|
|
|
if installed {
|
|
|
|
|
ver := database.GetPackageVersion(key, destDir)
|
|
|
|
|
if versionCompare(p.Version, ver) > 0 {
|
|
|
|
|
add(key)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slices.Sort(out)
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func FindPackages(name string, destDir string) []Package {
|
|
|
|
|
var found []Package
|
|
|
|
|
|
|
|
|
|
name = strings.ToLower(name)
|
|
|
|
|
|
|
|
|
|
var repo string
|
|
|
|
|
if strings.Contains(name, "/") {
|
|
|
|
|
parts := strings.SplitN(name, "/", 2)
|
|
|
|
|
repo = parts[0]
|
|
|
|
|
name = parts[1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, pkg := range GetPackages(destDir) {
|
|
|
|
|
if repo != "" && pkg.Repo != repo {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if strings.Contains(
|
|
|
|
|
strings.ToLower(pkg.Name),
|
|
|
|
|
name,
|
|
|
|
|
) {
|
|
|
|
|
found = append(found, pkg)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return found
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getPackage(name string, destDir string) (*Package, error) {
|
|
|
|
|
name = strings.ToLower(name)
|
|
|
|
|
|
|
|
|
|
var repo, pkgName string
|
|
|
|
|
if strings.Contains(name, "/") {
|
|
|
|
|
parts := strings.SplitN(name, "/", 2)
|
|
|
|
|
repo = parts[0]
|
|
|
|
|
pkgName = parts[1]
|
|
|
|
|
} else {
|
|
|
|
|
pkgName = name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var exact []Package
|
|
|
|
|
for _, p := range GetPackages(destDir) {
|
|
|
|
|
if strings.ToLower(p.Name) != pkgName {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if repo != "" && strings.ToLower(p.Repo) != repo {
|
2026-05-05 23:25:59 +03:00
|
|
|
continue
|
|
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
exact = append(exact, p)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(exact) == 0 {
|
|
|
|
|
return nil, errors.New("no packages found with name " + name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(exact) == 1 {
|
|
|
|
|
return &exact[0], nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Println("found multiple packages:")
|
|
|
|
|
|
|
|
|
|
for _, pkg := range exact {
|
|
|
|
|
fmt.Printf(
|
|
|
|
|
"%s/%s - %s\n",
|
|
|
|
|
pkg.Repo,
|
|
|
|
|
pkg.Name,
|
|
|
|
|
pkg.Version,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil, errors.New(
|
|
|
|
|
"multiple packages found, specify repository/name",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func manifestString(p *Package) string {
|
|
|
|
|
var b strings.Builder
|
|
|
|
|
b.WriteString("repo: " + p.Repo + "\n")
|
|
|
|
|
b.WriteString("name: " + p.Name + "\n")
|
|
|
|
|
b.WriteString("ver: " + p.Version + "\n")
|
|
|
|
|
if p.Desc != "" {
|
|
|
|
|
b.WriteString("desc: " + p.Desc + "\n")
|
|
|
|
|
}
|
|
|
|
|
if len(p.Depends) > 0 {
|
|
|
|
|
b.WriteString("deps: " + strings.Join(p.Depends, " ") + "\n")
|
|
|
|
|
}
|
|
|
|
|
if len(p.Conflicts) > 0 {
|
|
|
|
|
b.WriteString("conflicts: " + strings.Join(p.Conflicts, " ") + "\n")
|
|
|
|
|
}
|
|
|
|
|
if p.Type != "" {
|
|
|
|
|
b.WriteString("type: " + p.Type + "\n")
|
|
|
|
|
}
|
|
|
|
|
if p.URL != "" {
|
|
|
|
|
b.WriteString("url: " + p.URL + "\n")
|
|
|
|
|
}
|
|
|
|
|
return b.String()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func tarZstd(payloadDir, destPath, manifest string) error {
|
|
|
|
|
out, err := os.Create(destPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer out.Close()
|
|
|
|
|
|
|
|
|
|
zw, err := zstd.NewWriter(out)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tw := tar.NewWriter(zw)
|
|
|
|
|
|
|
|
|
|
hdr := &tar.Header{
|
|
|
|
|
Name: "manifest",
|
|
|
|
|
Mode: 0644,
|
|
|
|
|
Size: int64(len(manifest)),
|
|
|
|
|
}
|
|
|
|
|
if err := tw.WriteHeader(hdr); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if _, err := tw.Write([]byte(manifest)); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := writeTarTree(payloadDir, tw, "payload"); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := tw.Close(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return zw.Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func tarPayload(payloadDir, destPath string) error {
|
|
|
|
|
out, err := os.Create(destPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer out.Close()
|
|
|
|
|
|
|
|
|
|
zw, err := zstd.NewWriter(out)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tw := tar.NewWriter(zw)
|
|
|
|
|
|
|
|
|
|
if err := writeTarTree(payloadDir, tw, ""); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := tw.Close(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return zw.Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeTarTree(payloadDir string, tw *tar.Writer, prefix string) error {
|
|
|
|
|
return filepath.Walk(payloadDir, func(path string, info os.FileInfo, err error) error {
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rel, err := filepath.Rel(payloadDir, path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
name := filepath.Join(prefix, filepath.ToSlash(rel))
|
|
|
|
|
|
|
|
|
|
switch {
|
|
|
|
|
case info.Mode()&os.ModeSymlink != 0:
|
|
|
|
|
link, err := os.Readlink(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return tw.WriteHeader(&tar.Header{
|
|
|
|
|
Name: name,
|
|
|
|
|
Mode: int64(info.Mode()),
|
|
|
|
|
Typeflag: tar.TypeSymlink,
|
|
|
|
|
Linkname: link,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
case info.IsDir():
|
|
|
|
|
return tw.WriteHeader(&tar.Header{
|
|
|
|
|
Name: name + "/",
|
|
|
|
|
Mode: int64(info.Mode()),
|
|
|
|
|
Typeflag: tar.TypeDir,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
hdr := &tar.Header{
|
|
|
|
|
Name: name,
|
|
|
|
|
Mode: int64(info.Mode()),
|
|
|
|
|
Size: info.Size(),
|
|
|
|
|
ModTime: info.ModTime(),
|
|
|
|
|
}
|
|
|
|
|
if err := tw.WriteHeader(hdr); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
f, err := os.Open(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
_, err = io.Copy(tw, f)
|
|
|
|
|
f.Close()
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func readBuildConfig(destDir string) map[string]string {
|
|
|
|
|
cfg := map[string]string{}
|
|
|
|
|
cfgPath := filepath.Join(destDir, paths.BuildConfigFile)
|
|
|
|
|
data, err := os.ReadFile(cfgPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return cfg
|
|
|
|
|
}
|
|
|
|
|
m := uzbekdb.Loads(string(data), "map")
|
|
|
|
|
if m == nil {
|
|
|
|
|
return cfg
|
|
|
|
|
}
|
|
|
|
|
mp, ok := m.(map[interface{}][]interface{})
|
|
|
|
|
if !ok {
|
|
|
|
|
return cfg
|
|
|
|
|
}
|
|
|
|
|
for k, vals := range mp {
|
|
|
|
|
if len(vals) > 0 {
|
|
|
|
|
cfg[fmt.Sprintf("%v", k)] = fmt.Sprintf("%v", vals[0])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return cfg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func buildPackage(pkg *Package, destDir string) (string, error) {
|
|
|
|
|
buildScript := filepath.Join(pkg.Path, "build.sh")
|
|
|
|
|
if _, err := os.Stat(buildScript); err != nil {
|
|
|
|
|
return "", errors.New(
|
|
|
|
|
"package has no build.sh",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
payloadDir := filepath.Join(
|
|
|
|
|
destDir,
|
|
|
|
|
"tmp",
|
|
|
|
|
"build_"+pkg.Repo+"_"+pkg.Name,
|
|
|
|
|
)
|
|
|
|
|
os.RemoveAll(payloadDir)
|
|
|
|
|
os.MkdirAll(payloadDir, 0755)
|
|
|
|
|
|
|
|
|
|
env := append([]string{}, os.Environ()...)
|
|
|
|
|
for k, v := range readBuildConfig(destDir) {
|
|
|
|
|
env = append(env, k+"="+v)
|
|
|
|
|
}
|
|
|
|
|
env = append(
|
|
|
|
|
env,
|
|
|
|
|
"DESTDIR="+payloadDir,
|
|
|
|
|
"PKGURL="+pkg.URL,
|
|
|
|
|
"ARCH="+runtime.GOARCH,
|
|
|
|
|
"PKGREPO="+pkg.Repo,
|
|
|
|
|
"PKGVER="+pkg.Version,
|
|
|
|
|
"PKGNAME="+pkg.Name,
|
|
|
|
|
)
|
|
|
|
|
if v := os.Getenv("MAKEOPTS"); v != "" {
|
|
|
|
|
env = append(env, "MAKEOPTS="+v)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd := exec.Command("sh", "build.sh")
|
|
|
|
|
cmd.Dir = pkg.Path
|
|
|
|
|
cmd.Env = env
|
|
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
|
cmd.Stderr = os.Stderr
|
|
|
|
|
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entries, err := os.ReadDir(payloadDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
if len(entries) == 0 {
|
|
|
|
|
return "", errors.New(
|
|
|
|
|
"build.sh produced no files",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return payloadDir, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isSafePath(p string) bool {
|
|
|
|
|
p = filepath.ToSlash(p)
|
|
|
|
|
if p == "" || p == "/" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, part := range strings.Split(p, "/") {
|
|
|
|
|
if part == "." || part == ".." {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func conflictCheck(self string, conflicts []string, destDir string) (string, bool) {
|
|
|
|
|
for _, c := range conflicts {
|
|
|
|
|
if strings.Contains(c, "/") {
|
|
|
|
|
if c == self {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if database.IsInstalled(c, destDir) || installing[c] {
|
|
|
|
|
return c, true
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for key := range database.ReadInstalled(destDir) {
|
|
|
|
|
ks := fmt.Sprintf("%v", key)
|
|
|
|
|
if ks != self && strings.HasSuffix(ks, "/"+c) {
|
|
|
|
|
return ks, true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for k := range installing {
|
|
|
|
|
if k != self && strings.HasSuffix(k, "/"+c) {
|
|
|
|
|
return k, true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func installPayload(payloadDir, destDir string, pkg *Package, record bool) error {
|
|
|
|
|
err := helpers.CopyDir(payloadDir, destDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf(
|
|
|
|
|
"copy failed: %w",
|
|
|
|
|
err,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !record {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
outputFilePath := filepath.Join(
|
|
|
|
|
destDir,
|
|
|
|
|
paths.FilesDir,
|
|
|
|
|
pkg.Repo+"/"+pkg.Name+".files",
|
|
|
|
|
)
|
|
|
|
|
os.MkdirAll(filepath.Dir(outputFilePath), 0755)
|
|
|
|
|
|
|
|
|
|
outputFile, err := os.Create(outputFilePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer outputFile.Close()
|
|
|
|
|
|
|
|
|
|
err = filepath.WalkDir(
|
|
|
|
|
payloadDir,
|
|
|
|
|
func(path string, d os.DirEntry, err error) error {
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if d.IsDir() {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
rel, err := filepath.Rel(payloadDir, path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if !isSafePath(rel) {
|
|
|
|
|
return errors.New(
|
|
|
|
|
"unsafe path in payload: " + rel,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
_, err = outputFile.WriteString(
|
|
|
|
|
"/"+filepath.ToSlash(rel)+"\n",
|
|
|
|
|
)
|
|
|
|
|
return err
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func qpmpManifest(qpmpPath string) (string, error) {
|
|
|
|
|
f, err := os.Open(qpmpPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
|
|
zr, err := zstd.NewReader(f)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
defer zr.Close()
|
|
|
|
|
|
|
|
|
|
tr := tar.NewReader(zr)
|
|
|
|
|
for {
|
|
|
|
|
hdr, err := tr.Next()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
if hdr.Name == "manifest" {
|
|
|
|
|
data, err := io.ReadAll(tr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return string(data), nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func qpmpVersion(qpmpPath string) (string, error) {
|
|
|
|
|
data, err := qpmpManifest(qpmpPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
for _, line := range strings.Split(data, "\n") {
|
|
|
|
|
line = strings.TrimSpace(line)
|
|
|
|
|
if strings.HasPrefix(line, "ver:") {
|
|
|
|
|
return strings.TrimSpace(strings.TrimPrefix(line, "ver:")), nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "", errors.New("no ver in qpmp manifest")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func Build(name string, args []string, destDir string) error {
|
|
|
|
|
pkg, err := getPackage(name, destDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if pkg.Type != "archive" && pkg.Type != "binary" {
|
|
|
|
|
return errors.New(
|
|
|
|
|
"cannot build package of type " + pkg.Type,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := os.Stat(filepath.Join(pkg.Path, "build.sh")); err != nil {
|
|
|
|
|
return errors.New(
|
|
|
|
|
pkg.Repo + "/" + pkg.Name + " has no build.sh",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Println(
|
|
|
|
|
":: running build.sh for",
|
|
|
|
|
pkg.Repo+"/"+pkg.Name,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
payloadDir, err := buildPackage(pkg, destDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer os.RemoveAll(payloadDir)
|
|
|
|
|
|
|
|
|
|
builtDir := filepath.Join(
|
|
|
|
|
destDir,
|
|
|
|
|
paths.BuiltDir,
|
|
|
|
|
runtime.GOARCH,
|
|
|
|
|
pkg.Repo,
|
|
|
|
|
)
|
|
|
|
|
os.MkdirAll(builtDir, 0755)
|
|
|
|
|
|
|
|
|
|
if slices.Contains(args, ":qpmp") {
|
|
|
|
|
qpmpPath := filepath.Join(builtDir, pkg.Name+".qpmp")
|
|
|
|
|
|
|
|
|
|
err = tarZstd(
|
|
|
|
|
payloadDir,
|
|
|
|
|
qpmpPath,
|
|
|
|
|
manifestString(pkg),
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Println(
|
|
|
|
|
":: built",
|
|
|
|
|
pkg.Repo+"/"+pkg.Name,
|
|
|
|
|
"->",
|
|
|
|
|
qpmpPath,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tarPath := filepath.Join(builtDir, pkg.Name+".tar.zst")
|
|
|
|
|
|
|
|
|
|
err = tarPayload(payloadDir, tarPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Println(
|
|
|
|
|
":: built",
|
|
|
|
|
pkg.Repo+"/"+pkg.Name,
|
|
|
|
|
"->",
|
|
|
|
|
tarPath,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func installQpmp(qpmpPath string, args []string, destDir string) error {
|
|
|
|
|
stagingDir := filepath.Join(
|
|
|
|
|
destDir,
|
|
|
|
|
"tmp",
|
|
|
|
|
"qpmp_"+filepath.Base(qpmpPath),
|
|
|
|
|
)
|
|
|
|
|
os.RemoveAll(stagingDir)
|
|
|
|
|
os.MkdirAll(stagingDir, 0755)
|
|
|
|
|
defer os.RemoveAll(stagingDir)
|
|
|
|
|
|
|
|
|
|
file, err := os.Open(qpmpPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
|
|
err = extract.Zstd(
|
|
|
|
|
context.TODO(),
|
|
|
|
|
file,
|
|
|
|
|
stagingDir,
|
|
|
|
|
nil,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pkg, err := readPackage("", stagingDir, "")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if pkg.Name == "" {
|
|
|
|
|
pkg.Name = strings.TrimSuffix(
|
|
|
|
|
filepath.Base(qpmpPath),
|
|
|
|
|
".qpmp",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if pkg.Repo == "" {
|
|
|
|
|
pkg.Repo = "local"
|
|
|
|
|
}
|
|
|
|
|
pkg.Path = stagingDir
|
|
|
|
|
|
|
|
|
|
key := pkg.Repo + "/" + pkg.Name
|
|
|
|
|
|
|
|
|
|
noLog := slices.Contains(args, ":nl")
|
|
|
|
|
|
|
|
|
|
if slices.Contains(args, ":a") {
|
|
|
|
|
if !helpers.Ask(
|
|
|
|
|
"install " + key +
|
|
|
|
|
" with version " +
|
|
|
|
|
pkg.Version + "?",
|
|
|
|
|
) {
|
|
|
|
|
return errors.New("canceled by user")
|
2026-05-05 23:25:59 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-25 14:41:05 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
payloadDir := filepath.Join(stagingDir, "payload")
|
|
|
|
|
if _, err := os.Stat(payloadDir); err != nil {
|
|
|
|
|
return errors.New(
|
|
|
|
|
"qpmp has no payload",
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-05-05 23:25:59 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
fmt.Println(
|
|
|
|
|
":: installing " +
|
|
|
|
|
key,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
err = helpers.CopyDir(payloadDir, destDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf(
|
|
|
|
|
"copy failed: %w",
|
|
|
|
|
err,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
filesPath := filepath.Join(
|
|
|
|
|
destDir,
|
|
|
|
|
paths.FilesDir,
|
|
|
|
|
key+".files",
|
|
|
|
|
)
|
|
|
|
|
if noLog {
|
|
|
|
|
fmt.Println(
|
|
|
|
|
":: " + key +
|
|
|
|
|
" (" + pkg.Version + ") installed",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
os.MkdirAll(filepath.Dir(filesPath), 0755)
|
|
|
|
|
|
|
|
|
|
outputFile, err := os.Create(filesPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2026-03-25 14:41:05 +03:00
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
defer outputFile.Close()
|
2026-05-05 23:25:59 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
err = filepath.WalkDir(
|
|
|
|
|
payloadDir,
|
|
|
|
|
func(path string, d os.DirEntry, err error) error {
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2026-03-25 14:41:05 +03:00
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
if d.IsDir() {
|
|
|
|
|
return nil
|
2026-03-25 14:41:05 +03:00
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
rel, err := filepath.Rel(payloadDir, path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if !isSafePath(rel) {
|
|
|
|
|
return errors.New(
|
|
|
|
|
"unsafe path in qpmp payload: " + rel,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
_, err = outputFile.WriteString(
|
|
|
|
|
"/"+filepath.ToSlash(rel)+"\n",
|
|
|
|
|
)
|
|
|
|
|
return err
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2026-03-25 14:41:05 +03:00
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
outputFile.Close()
|
2026-03-25 14:41:05 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
err = database.RegPackage(
|
|
|
|
|
key,
|
|
|
|
|
pkg.Version,
|
|
|
|
|
destDir,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2026-03-25 14:41:05 +03:00
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
|
|
|
|
|
fmt.Println(
|
|
|
|
|
":: " + key +
|
|
|
|
|
" (" + pkg.Version + ") installed",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return nil
|
2026-03-25 14:41:05 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func Install(name string, args []string, destDir string) error {
|
2026-08-11 18:58:45 +03:00
|
|
|
if strings.HasSuffix(name, ".qpmp") {
|
|
|
|
|
if _, err := os.Stat(name); err == nil {
|
|
|
|
|
return installQpmp(name, args, destDir)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 14:41:05 +03:00
|
|
|
reinstall := slices.Contains(args, ":r")
|
|
|
|
|
disableDeps := slices.Contains(args, ":nd")
|
2026-08-11 18:58:45 +03:00
|
|
|
noLog := slices.Contains(args, ":nl")
|
|
|
|
|
force := slices.Contains(args, ":f")
|
|
|
|
|
|
2026-03-25 14:41:05 +03:00
|
|
|
pkg, err := getPackage(name, destDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
key := pkg.Repo + "/" + pkg.Name
|
2026-07-15 14:33:08 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
if _, ok := installing[key]; ok {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
installing[key] = true
|
|
|
|
|
defer delete(installing, key)
|
|
|
|
|
|
|
|
|
|
if !force {
|
|
|
|
|
if matched, ok := conflictCheck(key, pkg.Conflicts, destDir); ok {
|
|
|
|
|
return errors.New(
|
|
|
|
|
key + " conflicts with " + matched,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-15 14:33:08 +03:00
|
|
|
|
2026-03-25 14:41:05 +03:00
|
|
|
if !disableDeps {
|
2026-08-11 18:58:45 +03:00
|
|
|
if len(pkg.Depends) > 0 {
|
|
|
|
|
fmt.Println(
|
|
|
|
|
helpers.ToString(len(pkg.Depends)) +
|
|
|
|
|
" deps for " +
|
|
|
|
|
key + ":",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for i, dep := range pkg.Depends {
|
|
|
|
|
if dep == pkg.Name || dep == key {
|
|
|
|
|
return errors.New(
|
|
|
|
|
"broken deps in package " + dep,
|
|
|
|
|
)
|
2026-03-25 14:41:05 +03:00
|
|
|
}
|
2026-05-05 23:25:59 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
depPkgs := FindPackages(dep, destDir)
|
|
|
|
|
|
|
|
|
|
version := "not found"
|
2026-05-05 23:25:59 +03:00
|
|
|
if len(depPkgs) > 0 {
|
2026-08-11 18:58:45 +03:00
|
|
|
version = depPkgs[0].Version
|
2026-05-04 14:07:50 +03:00
|
|
|
}
|
2026-05-05 23:25:59 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
fmt.Printf(
|
|
|
|
|
"%d. %s - %s\n",
|
|
|
|
|
i+1,
|
|
|
|
|
dep,
|
|
|
|
|
version,
|
|
|
|
|
)
|
2026-03-25 14:41:05 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
|
2026-03-25 14:41:05 +03:00
|
|
|
if slices.Contains(args, ":a") {
|
2026-08-11 18:58:45 +03:00
|
|
|
if !helpers.Ask(
|
|
|
|
|
"install " +
|
|
|
|
|
key +
|
|
|
|
|
" with version " +
|
|
|
|
|
pkg.Version,
|
|
|
|
|
) {
|
2026-03-25 14:41:05 +03:00
|
|
|
return errors.New("canceled by user")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
|
2026-03-25 14:41:05 +03:00
|
|
|
if !disableDeps {
|
2026-08-11 18:58:45 +03:00
|
|
|
for _, dep := range pkg.Depends {
|
2026-05-05 23:25:59 +03:00
|
|
|
depPkg, err := getPackage(dep, destDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fullName := depPkg.Repo + "/" + depPkg.Name
|
|
|
|
|
|
|
|
|
|
if !database.IsInstalled(fullName, destDir) || reinstall {
|
|
|
|
|
err := Install(fullName, args, destDir)
|
2026-03-25 14:41:05 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
fmt.Println(
|
|
|
|
|
":: installing " +
|
|
|
|
|
key,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
builtQpmp := filepath.Join(
|
|
|
|
|
destDir,
|
|
|
|
|
paths.BuiltDir,
|
|
|
|
|
runtime.GOARCH,
|
|
|
|
|
pkg.Repo,
|
|
|
|
|
pkg.Name+".qpmp",
|
|
|
|
|
)
|
|
|
|
|
if _, err := os.Stat(builtQpmp); err == nil {
|
|
|
|
|
if ver, err := qpmpVersion(builtQpmp); err == nil && ver == pkg.Version {
|
|
|
|
|
fmt.Println(
|
|
|
|
|
":: found built",
|
|
|
|
|
key,
|
|
|
|
|
ver,
|
|
|
|
|
"-> installing from .qpmp",
|
|
|
|
|
)
|
|
|
|
|
return installQpmp(builtQpmp, args, destDir)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if pkg.Type == "archive" {
|
|
|
|
|
destArchivePath := filepath.Join(
|
|
|
|
|
destDir,
|
|
|
|
|
"tmp",
|
|
|
|
|
pkg.Name+"_"+pkg.Repo+".tar.zst",
|
|
|
|
|
)
|
2026-07-15 14:33:08 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
fullDestPath := filepath.Join(
|
|
|
|
|
pkg.Path,
|
|
|
|
|
"extract",
|
|
|
|
|
)
|
2026-07-15 14:33:08 +03:00
|
|
|
|
|
|
|
|
os.MkdirAll(filepath.Dir(destArchivePath), 0755)
|
2026-08-11 18:58:45 +03:00
|
|
|
defer os.Remove(destArchivePath)
|
|
|
|
|
defer os.RemoveAll(fullDestPath)
|
2026-07-15 14:33:08 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
client := helpers.HTTPClient()
|
2026-07-15 14:33:08 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
download := func() error {
|
|
|
|
|
resp, err := client.Get(pkg.URL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
return fmt.Errorf(
|
|
|
|
|
"download failed: HTTP %d",
|
|
|
|
|
resp.StatusCode,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f, err := os.Create(destArchivePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
_, err = io.Copy(f, resp.Body)
|
|
|
|
|
f.Close()
|
2026-07-15 14:33:08 +03:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
err := download()
|
2026-07-15 14:33:08 +03:00
|
|
|
if err != nil {
|
2026-08-11 18:58:45 +03:00
|
|
|
fmt.Println(
|
|
|
|
|
"!! failed to download",
|
|
|
|
|
pkg.URL,
|
|
|
|
|
"-> building from source",
|
|
|
|
|
)
|
|
|
|
|
payloadDir, berr := buildPackage(pkg, destDir)
|
|
|
|
|
if berr != nil {
|
|
|
|
|
return fmt.Errorf(
|
|
|
|
|
"download failed: %w; build fallback failed: %v",
|
|
|
|
|
err,
|
|
|
|
|
berr,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
defer os.RemoveAll(payloadDir)
|
|
|
|
|
fullDestPath = payloadDir
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Println(
|
|
|
|
|
":: downloaded",
|
|
|
|
|
pkg.URL,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
os.RemoveAll(fullDestPath)
|
|
|
|
|
|
|
|
|
|
file, err := os.Open(destArchivePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
|
|
err = extract.Zstd(
|
|
|
|
|
context.TODO(),
|
|
|
|
|
file,
|
|
|
|
|
fullDestPath,
|
|
|
|
|
nil,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-07-15 14:33:08 +03:00
|
|
|
}
|
|
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
fmt.Println(":: copying files to /")
|
2026-07-15 14:33:08 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
err = installPayload(fullDestPath, destDir, pkg, !noLog)
|
2026-07-15 14:33:08 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
} else if pkg.Type == "script" {
|
|
|
|
|
cmd := exec.Command("sh", "install.sh")
|
|
|
|
|
|
|
|
|
|
cmd.Dir = pkg.Path
|
|
|
|
|
cmd.Env = append(
|
|
|
|
|
os.Environ(),
|
|
|
|
|
"DESTDIR="+destDir,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
|
cmd.Stderr = os.Stderr
|
|
|
|
|
|
|
|
|
|
err := cmd.Run()
|
2026-07-15 14:33:08 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
} else if pkg.Type == "binary" {
|
|
|
|
|
outPath := filepath.Join(
|
|
|
|
|
destDir,
|
|
|
|
|
"usr/bin",
|
|
|
|
|
pkg.Name,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
os.MkdirAll(filepath.Dir(outPath), 0755)
|
2026-07-15 14:33:08 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
client := helpers.HTTPClient()
|
2026-07-15 14:33:08 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
resp, err := client.Get(pkg.URL)
|
|
|
|
|
downloadErr := error(nil)
|
2026-07-15 14:33:08 +03:00
|
|
|
if err != nil {
|
2026-08-11 18:58:45 +03:00
|
|
|
downloadErr = err
|
|
|
|
|
} else if resp.StatusCode != http.StatusOK {
|
|
|
|
|
downloadErr = fmt.Errorf(
|
|
|
|
|
"download failed: HTTP %d",
|
|
|
|
|
resp.StatusCode,
|
|
|
|
|
)
|
2026-07-15 14:33:08 +03:00
|
|
|
} else {
|
2026-08-11 18:58:45 +03:00
|
|
|
f, cerr := os.Create(outPath)
|
|
|
|
|
if cerr != nil {
|
|
|
|
|
downloadErr = cerr
|
|
|
|
|
} else {
|
|
|
|
|
_, cerr = io.Copy(f, resp.Body)
|
|
|
|
|
f.Close()
|
|
|
|
|
if cerr != nil {
|
|
|
|
|
downloadErr = cerr
|
|
|
|
|
} else if cerr = os.Chmod(outPath, 0755); cerr != nil {
|
|
|
|
|
downloadErr = cerr
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-15 14:33:08 +03:00
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
if resp != nil {
|
|
|
|
|
resp.Body.Close()
|
2026-07-15 14:33:08 +03:00
|
|
|
}
|
|
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
if downloadErr != nil {
|
|
|
|
|
fmt.Println(
|
|
|
|
|
"!! failed to download",
|
|
|
|
|
pkg.URL,
|
|
|
|
|
"-> building from source",
|
|
|
|
|
)
|
|
|
|
|
payloadDir, berr := buildPackage(pkg, destDir)
|
|
|
|
|
if berr != nil {
|
|
|
|
|
return fmt.Errorf(
|
|
|
|
|
"download failed: %w; build fallback failed: %v",
|
|
|
|
|
downloadErr,
|
|
|
|
|
berr,
|
|
|
|
|
)
|
2026-07-15 14:33:08 +03:00
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
defer os.RemoveAll(payloadDir)
|
2026-07-15 14:33:08 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
fmt.Println(":: copying files to /")
|
|
|
|
|
|
|
|
|
|
if err := installPayload(payloadDir, destDir, pkg, !noLog); err != nil {
|
2026-07-15 14:33:08 +03:00
|
|
|
return err
|
|
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
} else {
|
|
|
|
|
if !noLog {
|
|
|
|
|
outputFilePath := filepath.Join(
|
|
|
|
|
destDir,
|
|
|
|
|
paths.FilesDir,
|
|
|
|
|
pkg.Repo+"/"+pkg.Name+".files",
|
|
|
|
|
)
|
|
|
|
|
os.MkdirAll(filepath.Dir(outputFilePath), 0755)
|
2026-07-15 14:33:08 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
outputFile, err := os.Create(outputFilePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer outputFile.Close()
|
2026-07-15 14:33:08 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
_, err = outputFile.WriteString(
|
|
|
|
|
"/usr/bin/" + pkg.Name + "\n",
|
|
|
|
|
)
|
2026-07-15 14:33:08 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-15 14:33:08 +03:00
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
return errors.New(
|
|
|
|
|
"unknown package type: " + pkg.Type,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if pkg.After != "" {
|
|
|
|
|
fmt.Println(
|
|
|
|
|
":: running after script",
|
|
|
|
|
pkg.After,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
cmd := exec.Command(
|
|
|
|
|
"sh",
|
|
|
|
|
pkg.After,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
cmd.Dir = pkg.Path
|
|
|
|
|
cmd.Env = append(
|
|
|
|
|
os.Environ(),
|
|
|
|
|
"DESTDIR="+destDir,
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-25 14:41:05 +03:00
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
|
cmd.Stderr = os.Stderr
|
2026-08-11 18:58:45 +03:00
|
|
|
|
2026-03-25 14:41:05 +03:00
|
|
|
err := cmd.Run()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if !noLog {
|
|
|
|
|
err = database.RegPackage(
|
|
|
|
|
pkg.Repo+"/"+pkg.Name,
|
|
|
|
|
pkg.Version,
|
|
|
|
|
destDir,
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-15 14:33:08 +03:00
|
|
|
if err != nil {
|
2026-08-11 18:58:45 +03:00
|
|
|
return errors.New(
|
|
|
|
|
"failed to register package: " +
|
|
|
|
|
err.Error(),
|
|
|
|
|
)
|
2026-07-15 14:33:08 +03:00
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if pkg.Message != "" {
|
|
|
|
|
dataMsg, err := os.ReadFile(
|
|
|
|
|
filepath.Join(pkg.Path, pkg.Message),
|
|
|
|
|
)
|
2026-07-15 14:33:08 +03:00
|
|
|
if err != nil {
|
2026-08-11 18:58:45 +03:00
|
|
|
return errors.New(
|
|
|
|
|
"failed to read message from package: " +
|
|
|
|
|
err.Error(),
|
|
|
|
|
)
|
2026-07-15 14:33:08 +03:00
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
fmt.Println(
|
|
|
|
|
":: message from "+pkg.Repo+"/"+pkg.Name+
|
|
|
|
|
" "+pkg.Version+":\n"+
|
|
|
|
|
string(dataMsg),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Println(
|
|
|
|
|
":: "+pkg.Repo+"/"+pkg.Name+
|
|
|
|
|
" ("+pkg.Version+") installed",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func removeLocal(name string, args []string, destDir string) error {
|
|
|
|
|
key := name
|
|
|
|
|
if !strings.Contains(key, "/") {
|
|
|
|
|
for k := range database.ReadInstalled(destDir) {
|
|
|
|
|
if kStr, ok := k.(string); ok {
|
|
|
|
|
if strings.HasSuffix(kStr, "/"+name) {
|
|
|
|
|
key = kStr
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-15 14:33:08 +03:00
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !database.IsInstalled(key, destDir) {
|
|
|
|
|
return errors.New(
|
|
|
|
|
"no packages found with name " + name,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if slices.Contains(args, ":a") {
|
|
|
|
|
if !helpers.Ask(
|
|
|
|
|
"remove " + key + "?",
|
|
|
|
|
) {
|
|
|
|
|
return errors.New(
|
|
|
|
|
"canceled by user",
|
|
|
|
|
)
|
2026-07-15 14:33:08 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
filesPath := filepath.Join(
|
|
|
|
|
destDir,
|
|
|
|
|
paths.FilesDir,
|
|
|
|
|
key+".files",
|
|
|
|
|
)
|
|
|
|
|
if _, err := os.Stat(filesPath); err != nil {
|
|
|
|
|
return errors.New(
|
|
|
|
|
"no files list for " + key,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f, err := os.Open(filesPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
|
|
|
|
|
|
|
|
for scanner.Scan() {
|
|
|
|
|
file := scanner.Text()
|
|
|
|
|
|
|
|
|
|
if file == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !isSafePath(file) {
|
|
|
|
|
fmt.Fprintf(
|
|
|
|
|
os.Stderr,
|
|
|
|
|
"w! skipping unsafe path: %s\n",
|
|
|
|
|
file,
|
|
|
|
|
)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
path := filepath.Join(destDir, file)
|
|
|
|
|
|
|
|
|
|
if _, err := os.Stat(path); err == nil {
|
|
|
|
|
fmt.Println(
|
|
|
|
|
"removing",
|
|
|
|
|
path,
|
|
|
|
|
)
|
|
|
|
|
if err := os.RemoveAll(path); err != nil {
|
|
|
|
|
fmt.Fprintf(
|
|
|
|
|
os.Stderr,
|
|
|
|
|
"failed removing %s: %v\n",
|
|
|
|
|
path,
|
|
|
|
|
err,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !slices.Contains(args, ":nl") {
|
|
|
|
|
err = database.UnRegPackage(
|
|
|
|
|
key,
|
|
|
|
|
destDir,
|
|
|
|
|
)
|
2026-07-15 14:33:08 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-03-25 14:41:05 +03:00
|
|
|
}
|
|
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
os.Remove(filesPath)
|
|
|
|
|
|
|
|
|
|
fmt.Println(
|
|
|
|
|
":: " + key + " removed",
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-25 14:41:05 +03:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func Remove(name string, args []string, destDir string) error {
|
|
|
|
|
pkg, err := getPackage(name, destDir)
|
|
|
|
|
if err != nil {
|
2026-08-11 18:58:45 +03:00
|
|
|
if strings.HasPrefix(
|
|
|
|
|
err.Error(),
|
|
|
|
|
"no packages found",
|
|
|
|
|
) {
|
|
|
|
|
return removeLocal(name, args, destDir)
|
|
|
|
|
}
|
2026-03-25 14:41:05 +03:00
|
|
|
return err
|
|
|
|
|
}
|
2026-05-05 23:25:59 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
key := pkg.Repo + "/" + pkg.Name
|
|
|
|
|
|
2026-03-25 14:41:05 +03:00
|
|
|
if slices.Contains(args, ":a") {
|
2026-08-11 18:58:45 +03:00
|
|
|
localVersion := database.GetPackageVersion(
|
|
|
|
|
key,
|
|
|
|
|
destDir,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if !helpers.Ask(
|
|
|
|
|
"remove " + key +
|
|
|
|
|
" repo version " +
|
|
|
|
|
pkg.Version +
|
|
|
|
|
" local version " +
|
|
|
|
|
localVersion +
|
|
|
|
|
"?",
|
|
|
|
|
) {
|
|
|
|
|
return errors.New(
|
|
|
|
|
"canceled by user",
|
|
|
|
|
)
|
2026-03-25 14:41:05 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
if pkg.Remove != "" {
|
|
|
|
|
fmt.Println(
|
|
|
|
|
":: running remove script " +
|
|
|
|
|
pkg.Remove +
|
|
|
|
|
" ...",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
cmd := exec.Command(
|
|
|
|
|
"sh",
|
|
|
|
|
pkg.Remove,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
cmd.Dir = pkg.Path
|
|
|
|
|
cmd.Env = append(
|
|
|
|
|
os.Environ(),
|
|
|
|
|
"DESTDIR="+destDir,
|
|
|
|
|
)
|
2026-07-15 14:33:08 +03:00
|
|
|
|
2026-03-25 14:41:05 +03:00
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
|
cmd.Stderr = os.Stderr
|
2026-08-11 18:58:45 +03:00
|
|
|
|
|
|
|
|
err := cmd.Run()
|
2026-03-25 14:41:05 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-05 23:25:59 +03:00
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
|
|
|
|
|
filesPath := filepath.Join(
|
|
|
|
|
destDir,
|
|
|
|
|
paths.FilesDir,
|
|
|
|
|
key+".files",
|
|
|
|
|
)
|
|
|
|
|
persistentFiles := true
|
|
|
|
|
if _, err := os.Stat(filesPath); err != nil {
|
|
|
|
|
filesPath = filepath.Join(
|
|
|
|
|
pkg.Path,
|
|
|
|
|
"files",
|
|
|
|
|
)
|
|
|
|
|
persistentFiles = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if _, err := os.Stat(filesPath); err == nil {
|
|
|
|
|
|
|
|
|
|
f, err := os.Open(filesPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-07-15 14:33:08 +03:00
|
|
|
defer f.Close()
|
|
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
|
2026-07-15 14:33:08 +03:00
|
|
|
scanner := bufio.NewScanner(f)
|
2026-08-11 18:58:45 +03:00
|
|
|
|
2026-07-15 14:33:08 +03:00
|
|
|
for scanner.Scan() {
|
|
|
|
|
file := scanner.Text()
|
|
|
|
|
|
|
|
|
|
if file == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
if !isSafePath(file) {
|
|
|
|
|
fmt.Fprintf(
|
|
|
|
|
os.Stderr,
|
|
|
|
|
"w! skipping unsafe path: %s\n",
|
|
|
|
|
file,
|
|
|
|
|
)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
path := filepath.Join(
|
|
|
|
|
destDir,
|
|
|
|
|
file,
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-15 14:33:08 +03:00
|
|
|
|
|
|
|
|
if _, err := os.Stat(path); err == nil {
|
|
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
fmt.Println(
|
|
|
|
|
"removing",
|
|
|
|
|
path,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
err := os.RemoveAll(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(
|
|
|
|
|
os.Stderr,
|
|
|
|
|
"failed removing %s: %v\n",
|
|
|
|
|
path,
|
|
|
|
|
err,
|
|
|
|
|
)
|
2026-07-15 14:33:08 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
|
2026-07-15 14:33:08 +03:00
|
|
|
if err := scanner.Err(); err != nil {
|
2026-08-11 18:58:45 +03:00
|
|
|
return err
|
2026-07-15 14:33:08 +03:00
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
|
|
|
|
|
} else if pkg.Remove == "" {
|
|
|
|
|
return errors.New(
|
|
|
|
|
"package has no files list and no remove script",
|
|
|
|
|
)
|
2026-07-15 14:33:08 +03:00
|
|
|
}
|
|
|
|
|
|
2026-08-11 18:58:45 +03:00
|
|
|
|
|
|
|
|
if !slices.Contains(args, ":nl") {
|
|
|
|
|
err = database.UnRegPackage(
|
|
|
|
|
key,
|
|
|
|
|
destDir,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2026-07-15 14:33:08 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-08-11 18:58:45 +03:00
|
|
|
|
|
|
|
|
if persistentFiles {
|
|
|
|
|
os.Remove(filesPath)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fmt.Println(
|
|
|
|
|
":: "+key+
|
|
|
|
|
" ("+pkg.Version+") removed",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func Upgrade(names []string, args []string, destDir string) error {
|
|
|
|
|
upgraded := 0
|
|
|
|
|
|
|
|
|
|
for _, n := range names {
|
|
|
|
|
pkg, err := getPackage(n, destDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Println("w! skipping", n, err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
key := pkg.Repo + "/" + pkg.Name
|
|
|
|
|
|
|
|
|
|
ver := database.GetPackageVersion(key, destDir)
|
|
|
|
|
if ver == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Println(
|
|
|
|
|
":: upgrading", key,
|
|
|
|
|
ver, "->", pkg.Version,
|
|
|
|
|
)
|
|
|
|
|
if err := Install(key, args, destDir); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
upgraded++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if upgraded == 0 {
|
|
|
|
|
fmt.Println(":: nothing to upgrade")
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Println("::", upgraded, "package(s) upgraded")
|
2026-03-25 14:41:05 +03:00
|
|
|
}
|
2026-05-04 14:07:50 +03:00
|
|
|
|
2026-05-05 23:25:59 +03:00
|
|
|
return nil
|
2026-03-25 14:41:05 +03:00
|
|
|
}
|