Files
qulay/package/package.go
T

1792 lines
31 KiB
Go
Raw Normal View History

2026-03-25 14:41:05 +03:00
package pkg
import (
"archive/tar"
2026-03-25 14:41:05 +03:00
"bufio"
"context"
2026-05-05 23:25:59 +03:00
"errors"
"fmt"
"io"
"net/http"
2026-03-25 14:41:05 +03:00
"os"
"os/exec"
"path/filepath"
"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"
"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 {
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
Message string
Path string
2026-03-25 14:41:05 +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 {
return nil, err
2026-03-25 14:41:05 +03:00
}
defer f.Close()
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-03-25 14:41:05 +03:00
for scanner.Scan() {
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
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
}
packagesPath := filepath.Join(
repoPath,
"packages",
)
packages, err := os.ReadDir(packagesPath)
2026-05-05 23:25:59 +03:00
if err != nil {
continue
}
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
}
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
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
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
}
defer outputFile.Close()
2026-05-05 23:25:59 +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
}
if d.IsDir() {
return nil
2026-03-25 14:41:05 +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
}
outputFile.Close()
2026-03-25 14:41:05 +03:00
err = database.RegPackage(
key,
pkg.Version,
destDir,
)
if err != nil {
return err
2026-03-25 14:41:05 +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 {
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")
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
}
key := pkg.Repo + "/" + pkg.Name
2026-07-15 14:33:08 +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 {
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
depPkgs := FindPackages(dep, destDir)
version := "not found"
2026-05-05 23:25:59 +03:00
if len(depPkgs) > 0 {
version = depPkgs[0].Version
2026-05-04 14:07:50 +03:00
}
2026-05-05 23:25:59 +03:00
fmt.Printf(
"%d. %s - %s\n",
i+1,
dep,
version,
)
2026-03-25 14:41:05 +03:00
}
}
}
2026-03-25 14:41:05 +03:00
if slices.Contains(args, ":a") {
if !helpers.Ask(
"install " +
key +
" with version " +
pkg.Version,
) {
2026-03-25 14:41:05 +03:00
return errors.New("canceled by user")
}
}
2026-03-25 14:41:05 +03:00
if !disableDeps {
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
}
}
}
}
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
fullDestPath := filepath.Join(
pkg.Path,
"extract",
)
2026-07-15 14:33:08 +03:00
os.MkdirAll(filepath.Dir(destArchivePath), 0755)
defer os.Remove(destArchivePath)
defer os.RemoveAll(fullDestPath)
2026-07-15 14:33:08 +03:00
client := helpers.HTTPClient()
2026-07-15 14:33:08 +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
}
err := download()
2026-07-15 14:33:08 +03:00
if err != 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",
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
}
fmt.Println(":: copying files to /")
2026-07-15 14:33:08 +03:00
err = installPayload(fullDestPath, destDir, pkg, !noLog)
2026-07-15 14:33:08 +03:00
if err != nil {
return err
}
} 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
}
} 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
client := helpers.HTTPClient()
2026-07-15 14:33:08 +03:00
resp, err := client.Get(pkg.URL)
downloadErr := error(nil)
2026-07-15 14:33:08 +03:00
if err != nil {
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 {
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
}
if resp != nil {
resp.Body.Close()
2026-07-15 14:33:08 +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
}
defer os.RemoveAll(payloadDir)
2026-07-15 14:33:08 +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
}
} 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
outputFile, err := os.Create(outputFilePath)
if err != nil {
return err
}
defer outputFile.Close()
2026-07-15 14:33:08 +03:00
_, err = outputFile.WriteString(
"/usr/bin/" + pkg.Name + "\n",
)
2026-07-15 14:33:08 +03:00
if err != nil {
return err
}
}
2026-07-15 14:33:08 +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-03-25 14:41:05 +03:00
err := cmd.Run()
if err != nil {
return err
}
}
if !noLog {
err = database.RegPackage(
pkg.Repo+"/"+pkg.Name,
pkg.Version,
destDir,
)
2026-07-15 14:33:08 +03:00
if err != nil {
return errors.New(
"failed to register package: " +
err.Error(),
)
2026-07-15 14:33:08 +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 {
return errors.New(
"failed to read message from package: " +
err.Error(),
)
2026-07-15 14:33:08 +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
}
}
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
}
}
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
}
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 {
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
key := pkg.Repo + "/" + pkg.Name
2026-03-25 14:41:05 +03:00
if slices.Contains(args, ":a") {
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
}
}
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
err := cmd.Run()
2026-03-25 14:41:05 +03:00
if err != nil {
return err
}
}
2026-05-05 23:25:59 +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-07-15 14:33:08 +03:00
scanner := bufio.NewScanner(f)
2026-07-15 14:33:08 +03:00
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,
)
2026-07-15 14:33:08 +03:00
if _, err := os.Stat(path); err == nil {
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-07-15 14:33:08 +03:00
if err := scanner.Err(); err != nil {
return err
2026-07-15 14:33:08 +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
}
if !slices.Contains(args, ":nl") {
err = database.UnRegPackage(
key,
destDir,
)
if err != nil {
return err
2026-07-15 14:33:08 +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
}