summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/paths.go5
-rw-r--r--database/database.go161
-rw-r--r--go.mod2
-rw-r--r--helpers/helpers.go17
-rw-r--r--main.go93
-rw-r--r--package/package.go1822
6 files changed, 1779 insertions, 321 deletions
diff --git a/config/paths.go b/config/paths.go
index ff3b927..d4ffcbd 100644
--- a/config/paths.go
+++ b/config/paths.go
@@ -1,5 +1,8 @@
package paths
-const ReposUrlsFile = "etc/qulay/repositories"
+const ReposConfigDir = "etc/qulay/repos/"
const ReposDir = "var/lib/qulay/repos/"
const InstalledFile = "var/lib/qulay/installed"
+const BuiltDir = "var/lib/qulay/built/"
+const FilesDir = "var/lib/qulay/files/"
+const BuildConfigFile = "etc/qulay/build"
diff --git a/database/database.go b/database/database.go
index 0624bb7..0e55205 100644
--- a/database/database.go
+++ b/database/database.go
@@ -1,70 +1,112 @@
package database
import (
- "fmt"
- "os"
- // "archive/tar"
- "bufio"
"context"
+ "fmt"
+ "io"
"net/http"
+ "os"
"path"
"path/filepath"
- "sync"
- "time"
- // "net/url"
- "io"
+ "slices"
"strings"
-
+ "sync"
+
"qulay/config"
- "codeberg.org/UzbekLinux/uzbekdb-go"
+ "qulay/helpers"
+ "codeberg.org/UzbekLinux/uzbekdb-go"
"github.com/codeclysm/extract/v3"
- // "github.com/klauspost/compress/zstd"
)
var _name string = filepath.Base(os.Args[0])
-func GetReposUrls(destDir string) []string {
- f, err := os.Open(filepath.Join(destDir, paths.ReposUrlsFile))
+type Repo struct {
+ Name string
+ URL string
+}
+
+func GetRepos(destDir string) []Repo {
+ dir := filepath.Join(destDir, paths.ReposConfigDir)
+
+ entries, err := os.ReadDir(dir)
if err != nil {
- return []string{}
- }
- defer f.Close()
- var urls []string
- scanner := bufio.NewScanner(f)
- for scanner.Scan() {
- urls = append(urls, scanner.Text())
+ return nil
}
- return urls
-}
-func DownloadRepos(urls []string, destDir string) {
- var wg sync.WaitGroup
- var links []string
+ var repos []Repo
+
+ for _, entry := range entries {
+ if entry.IsDir() {
+ continue
+ }
+
+ data, err := os.ReadFile(
+ filepath.Join(dir, entry.Name()),
+ )
+ if err != nil {
+ continue
+ }
- for _, link := range urls {
- link = strings.TrimSpace(link)
- if link == "" {
+ m := uzbekdb.Loads(string(data), "map")
+ if m == nil {
continue
}
- links = append(links, link)
+
+ mp, ok := m.(map[interface{}][]interface{})
+ if !ok {
+ continue
+ }
+
+ repo := Repo{Name: entry.Name()}
+
+ if vals, ok := mp["url"]; ok && len(vals) > 0 {
+ repo.URL = fmt.Sprintf("%v", vals[0])
+ }
+
+ repos = append(repos, repo)
+ }
+
+ slices.SortFunc(repos, func(a, b Repo) int {
+ return strings.Compare(a.Name, b.Name)
+ })
+
+ return repos
+}
+
+func RepoURL(destDir, name string) string {
+ for _, repo := range GetRepos(destDir) {
+ if repo.Name == name {
+ return repo.URL
+ }
}
- wg.Add(len(links))
- for _, link := range links {
- go func(link string) {
+ return ""
+}
+
+func DownloadRepos(destDir string) {
+ var wg sync.WaitGroup
+
+ for _, repo := range GetRepos(destDir) {
+ wg.Add(1)
+
+ go func(base, name string) {
defer wg.Done()
+
+ link := base
+ if !strings.HasSuffix(link, "/") {
+ link += "/"
+ }
+ link += name + ".tar.zst"
+
archiveName := path.Base(link)
- repoName := strings.TrimSuffix(archiveName, ".tar.zst")
destArchivePath := filepath.Join(destDir, "tmp", archiveName)
- destNArchivePath := filepath.Join(destDir, "tmp")
destPath := filepath.Join(destDir, paths.ReposDir)
- fullDestPath := filepath.Join(destDir, paths.ReposDir, repoName)
+ fullDestPath := filepath.Join(destPath, name)
- os.MkdirAll(destNArchivePath, 0755)
+ os.MkdirAll(filepath.Dir(destArchivePath), 0755)
+ defer os.Remove(destArchivePath)
- client := &http.Client{
- Timeout: 10 * time.Second,
- }
+ client := helpers.HTTPClient()
resp, err := client.Get(link)
if err != nil {
@@ -72,6 +114,10 @@ func DownloadRepos(urls []string, destDir string) {
return
}
defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ fmt.Println("!! http error", resp.StatusCode, link)
+ return
+ }
f, err := os.Create(destArchivePath)
if err != nil {
@@ -88,8 +134,8 @@ func DownloadRepos(urls []string, destDir string) {
fmt.Println(":: downloaded", archiveName, "to", destArchivePath)
- err = os.RemoveAll(fullDestPath)
fmt.Println(":: removing old repo", fullDestPath)
+ err = os.RemoveAll(fullDestPath)
if err != nil {
fmt.Println("w! failed to delete old repo", fullDestPath, err)
}
@@ -99,13 +145,16 @@ func DownloadRepos(urls []string, destDir string) {
fmt.Println("!! failed to open archive file", destArchivePath, err)
return
}
- err = extract.Zstd(context.TODO(), file, destPath, nil)
+ defer file.Close()
+
+ err = extract.Zstd(context.TODO(), file, fullDestPath, nil)
if err != nil {
fmt.Println("!! failed to extract", err)
return
}
- }(link)
+ }(repo.URL, repo.Name)
}
+
wg.Wait()
fmt.Println(":: repo updating ended")
}
@@ -123,7 +172,7 @@ func ReadInstalled(destDir string) map[interface{}][]interface{} {
}
data := uzbekdb.Loads(string(text), "map")
if data == nil {
- return map[interface{}][]interface{}{}
+ return map[interface{}][]interface{}{}
}
return data.(map[interface{}][]interface{})
}
@@ -150,18 +199,27 @@ func IsInstalled(name string, destDir string) bool {
func GetPackageVersion(name string, destDir string) string {
data := ReadInstalled(destDir)
+ if !strings.Contains(name, "/") {
+ for k := range data {
+ if kStr, ok := k.(string); ok {
+ if strings.HasSuffix(kStr, "/"+name) {
+ name = kStr
+ break
+ }
+ }
+ }
+ }
alo, _ := data[name]
if len(alo) > 0 {
- return alo[0].(string)
- } else {
- return ""
+ return fmt.Sprintf("%v", alo[0])
}
+ return ""
}
func RegPackage(name string, version string, destDir string) error {
data := ReadInstalled(destDir)
val, installed := data[name]
-
+
if installed && len(val) > 0 {
currentVersion := fmt.Sprintf("%v", val[0])
if currentVersion != version {
@@ -177,7 +235,7 @@ func UnRegPackage(name string, destDir string) error {
data := ReadInstalled(destDir)
_, ok := data[name]
if !ok {
- return fmt.Errorf("cant unregister '%s': package is not installed", name)
+ return nil
}
delete(data, name)
return WriteInstalled(data, destDir)
@@ -185,13 +243,12 @@ func UnRegPackage(name string, destDir string) error {
func PurgeRepository(name string, destDir string) error {
dirPath := filepath.Join(destDir, paths.ReposDir, name)
+ if _, err := os.Stat(dirPath); err != nil {
+ return fmt.Errorf("repository '%s' not found", name)
+ }
err := os.RemoveAll(dirPath)
if err != nil {
return err
}
return nil
}
-
-// func Ensure(args, destDir) error {
-//
-// }
diff --git a/go.mod b/go.mod
index fd76bbd..3b4d48f 100644
--- a/go.mod
+++ b/go.mod
@@ -5,13 +5,13 @@ go 1.26.4
require (
codeberg.org/UzbekLinux/uzbekdb-go v0.0.0-20260509165828-8f8f4cbf599e
github.com/codeclysm/extract/v3 v3.1.1
+ github.com/klauspost/compress v1.15.13
)
require (
github.com/h2non/filetype v1.1.3 // indirect
github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5 // indirect
github.com/juju/loggo v1.0.0 // indirect
- github.com/klauspost/compress v1.15.13 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect
diff --git a/helpers/helpers.go b/helpers/helpers.go
index 2b4763a..780dbf2 100644
--- a/helpers/helpers.go
+++ b/helpers/helpers.go
@@ -2,6 +2,8 @@ package helpers
import (
"fmt"
+ "net"
+ "net/http"
"strings"
"path/filepath"
"math/rand"
@@ -25,6 +27,18 @@ func Ask(str string) bool {
return true
}
+func HTTPClient() *http.Client {
+ transport := &http.Transport{
+ DialContext: (&net.Dialer{
+ Timeout: 10 * time.Second,
+ }).DialContext,
+ TLSHandshakeTimeout: 10 * time.Second,
+ }
+ return &http.Client{
+ Transport: transport,
+ }
+}
+
func ToString(v interface{}) string {
switch val := v.(type) {
case int:
@@ -67,6 +81,9 @@ func CopyDir(src, dst string) error {
if err != nil {
return err
}
+ if err := os.RemoveAll(target); err != nil {
+ return err
+ }
return os.Symlink(link, target)
case info.IsDir():
diff --git a/main.go b/main.go
index 74aabb4..6433ba3 100644
--- a/main.go
+++ b/main.go
@@ -13,29 +13,41 @@ import (
)
var _name string = filepath.Base(os.Args[0])
-var _cmds = []string{"h", "+", "-", "g", "s", "p", "u", "r", "q", "f", "v"}
-var _root_cmds = []string{"+", "-", "u", "i"}
+var _cmds = []string{"h", "+", "-", "g", "s", "p", "u", "r", "q", "f", "v", "b", "o"}
-const _version string = "0.7"
-const _help string = `{_name}: command-line interface to the Qulay Package Manager
+const _version string = "1.0"
+const _help string = `{_name}: Qulay Package Manager
Usage:
{_name} h -> help menu
- {_name} + pkg -> download and install package
+ {_name} + pkg -> download and install package (or .qpmp file path)
{_name} - pkg -> remove package
+ {_name} b pkg -> build package into .tar.zst (or .qpmp on :qpmp)
{_name} g pkg -> get package version
{_name} s pkg -> search packages by name
{_name} p repo -> purge local repo directory
{_name} u -> update all repositories
- {_name} r -> get all repositories URLs
+ {_name} r -> get all repositories configs
+ {_name} o -> upgrade all installed packages
{_name} q -> get all installed packages
{_name} f -> get files path for destDir
{_name} v -> get {_name} version
Options:
- :v -> verbose output
:a -> ask before doing
:nl -> disable recording in databases and logs
:nd -> do not download deps
:r -> reinstall already installed deps
+ :f -> force install (ignore conflicts)
+ :qpmp -> build a .qpmp archive instead of .tar.zst
+Tags (for +, - , b and o):
+ @all -> apply to all packages
+ repo/@all -> apply to all packages in repo
+ @inst -> apply to installed (for b: built) packages
+ repo/@inst -> apply to installed (for b: built) packages in repo
+ @notinst -> apply to not installed (for b: not built) packages
+ repo/@notinst-> apply to not installed (for b: not built) packages in repo
+ @old -> apply to installed/built packages outdated by version
+ repo/@old -> apply to outdated packages in repo
+ o without tags applies @old automatically
Vars:
DESTDIR -> path where the package will be installed/removed`
@@ -86,7 +98,12 @@ func main() {
fmt.Println("!! no package(s) received")
os.Exit(1)
}
- for _, n := range oths {
+ names, err := pkg.Expand("+", oths, absDestDir)
+ if err != nil {
+ fmt.Println("!!", err)
+ os.Exit(1)
+ }
+ for _, n := range names {
err := pkg.Install(n, opts, absDestDir)
if err != nil {
fmt.Println("!!", err)
@@ -98,7 +115,12 @@ func main() {
fmt.Println("!! no package(s) received")
os.Exit(1)
}
- for _, n := range oths {
+ names, err := pkg.Expand("-", oths, absDestDir)
+ if err != nil {
+ fmt.Println("!!", err)
+ os.Exit(1)
+ }
+ for _, n := range names {
err := pkg.Remove(n, opts, absDestDir)
if err != nil {
fmt.Println("!!", err)
@@ -106,7 +128,24 @@ func main() {
}
}
} else if cmds[0] == "u" {
- database.DownloadRepos(database.GetReposUrls(absDestDir), absDestDir)
+ database.DownloadRepos(absDestDir)
+ } else if cmds[0] == "b" {
+ if len(oths) < 1 {
+ fmt.Println("!! no package(s) received")
+ os.Exit(1)
+ }
+ names, err := pkg.Expand("b", oths, absDestDir)
+ if err != nil {
+ fmt.Println("!!", err)
+ os.Exit(1)
+ }
+ for _, n := range names {
+ err := pkg.Build(n, opts, absDestDir)
+ if err != nil {
+ fmt.Println("!!", err)
+ os.Exit(1)
+ }
+ }
} else if cmds[0] == "g" {
if len(oths) < 1 {
fmt.Println("!! no package(s) received")
@@ -116,21 +155,37 @@ func main() {
fmt.Println(database.GetPackageVersion(name, absDestDir))
}
} else if cmds[0] == "r" {
- for _, url := range database.GetReposUrls(absDestDir) {
- fmt.Println(url)
+ for _, repo := range database.GetRepos(absDestDir) {
+ fmt.Println(repo.Name, "url:", repo.URL)
+ }
+ } else if cmds[0] == "o" {
+ if len(oths) < 1 {
+ oths = []string{"@old"}
+ }
+ names, err := pkg.Expand("o", oths, absDestDir)
+ if err != nil {
+ fmt.Println("!!", err)
+ os.Exit(1)
+ }
+ err = pkg.Upgrade(names, opts, absDestDir)
+ if err != nil {
+ fmt.Println("!!", err)
+ os.Exit(1)
}
} else if cmds[0] == "q" {
for name, version := range database.ReadInstalled(absDestDir) {
- fmt.Println(name, "-", version[0])
+ if len(version) > 0 {
+ fmt.Println(name, "-", version[0])
+ }
}
} else if cmds[0] == "s" {
if len(oths) < 1 {
pkgs := pkg.GetPackages(absDestDir)
for _, p := range pkgs {
if database.IsInstalled(p.Repo+"/"+p.Name, absDestDir) {
- fmt.Printf("[+] %s/%s - %v - %v\n", p.Repo, p.Name, p.Data[0], p.Data[1])
+ fmt.Printf("[+] %s/%s - %v - %v\n", p.Repo, p.Name, p.Version, p.Desc)
} else {
- fmt.Printf(" %s/%s - %v - %v\n", p.Repo, p.Name, p.Data[0], p.Data[1])
+ fmt.Printf(" %s/%s - %v - %v\n", p.Repo, p.Name, p.Version, p.Desc)
}
}
} else {
@@ -138,9 +193,9 @@ func main() {
pkgs := pkg.FindPackages(n, absDestDir)
for _, p := range pkgs {
if database.IsInstalled(p.Repo+"/"+p.Name, absDestDir) {
- fmt.Printf("[+] %s/%s - %v - %v\n", p.Repo, p.Name, p.Data[0], p.Data[1])
+ fmt.Printf("[+] %s/%s - %v - %v\n", p.Repo, p.Name, p.Version, p.Desc)
} else {
- fmt.Printf(" %s/%s - %v - %v\n", p.Repo, p.Name, p.Data[0], p.Data[1])
+ fmt.Printf(" %s/%s - %v - %v\n", p.Repo, p.Name, p.Version, p.Desc)
}
}
}
@@ -166,9 +221,11 @@ func main() {
fmt.Printf("dest dir: %v\n", destDir)
fmt.Printf("abs dest dir: %v\n", absDestDir)
fmt.Printf("is abs: %v\n", filepath.IsAbs(destDir))
- fmt.Printf("repos urls file: %v\n", filepath.Join(absDestDir, paths.ReposUrlsFile))
+ fmt.Printf("repos config dir: %v\n", filepath.Join(absDestDir, paths.ReposConfigDir))
fmt.Printf("repos dir: %v\n", filepath.Join(absDestDir, paths.ReposDir))
fmt.Printf("installed pkgs database: %v\n", filepath.Join(absDestDir, paths.InstalledFile))
+ fmt.Printf("built pkgs dir: %v\n", filepath.Join(absDestDir, paths.BuiltDir))
+ fmt.Printf("build config file: %v\n", filepath.Join(absDestDir, paths.BuildConfigFile))
} else if cmds[0] == "v" {
fmt.Println(_version)
} else {
diff --git a/package/package.go b/package/package.go
index 79d6e04..6a2aff8 100644
--- a/package/package.go
+++ b/package/package.go
@@ -1,28 +1,28 @@
package pkg
import (
+ "archive/tar"
"bufio"
+ "context"
"errors"
"fmt"
- "slices"
- "strings"
-
- // "slices"
- // "strconv"
+ "io"
+ "net/http"
"os"
"os/exec"
"path/filepath"
+ "regexp"
+ "runtime"
+ "slices"
+ "strconv"
+ "strings"
paths "qulay/config"
"qulay/database"
"qulay/helpers"
- "net/http"
- "time"
- "io"
- "context"
-
- "github.com/codeclysm/extract/v3"
"codeberg.org/UzbekLinux/uzbekdb-go"
+ "github.com/codeclysm/extract/v3"
+ "github.com/klauspost/compress/zstd"
)
var _name string = filepath.Base(os.Args[0])
@@ -30,188 +30,1154 @@ var _name string = filepath.Base(os.Args[0])
var installing = map[string]bool{}
type Package struct {
- Name string
- Repo string
- Data []interface{}
-}
+ Name string
+ Repo string
+ Version string
+ Desc string
+
+ Depends []string
+ Type string
+ URL string
-func randomTempDir(destDir string) string {
- tempDir := "qulay_" + helpers.RandStr(10)
- tempDirPath := filepath.Join(destDir, tempDir)
- os.MkdirAll(tempDirPath, 0755)
- return tempDirPath
+ Conflicts []string
+
+ After string
+ Remove string
+
+ Message string
+
+ Path string
}
-func readPackage(path string) ([]string, string, string, string, string) {
- pkgPath := filepath.Join(path, "pkg")
-
- f, err := os.Open(pkgPath)
+func readPackage(repo string, path string, repoURL string) (*Package, error) {
+ f, err := os.Open(filepath.Join(path, "manifest"))
if err != nil {
- return []string{}, "", "", "", ""
+ return nil, err
}
defer f.Close()
- var depends []string
- var taip = ""
- var url = ""
- var afterScript = ""
- var removeScript = ""
+
+ pkg := &Package{
+ Repo: repo,
+ Path: path,
+ }
scanner := bufio.NewScanner(f)
+
for scanner.Scan() {
- line := scanner.Text()
- if strings.HasPrefix(line, "deps: ") {
- depends = append(depends, strings.Fields(strings.TrimPrefix(line, "deps: "))...)
- } else if strings.HasPrefix(line, "type: ") {
- taip = strings.TrimPrefix(line, "type: ")
- } else if strings.HasPrefix(line, "url: ") {
- url = strings.TrimPrefix(line, "url: ")
- } else if strings.HasPrefix(line, "after: ") {
- afterScript = strings.TrimPrefix(line, "after: ")
- } else if strings.HasPrefix(line, "remove: ") {
- removeScript = strings.TrimPrefix(line, "remove: ")
+ 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)
}
- return depends, taip, url, afterScript, removeScript
+ for i, conflict := range pkg.Conflicts {
+ pkg.Conflicts[i] = subst(conflict)
+ }
+
+ return pkg, nil
}
func GetPackages(destDir string) []Package {
var found []Package
- entries, _ := os.ReadDir(filepath.Join(destDir, paths.ReposDir))
- for _, e := range entries {
- if !e.IsDir() {
+
+ 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() {
continue
}
- data, err := os.ReadFile(filepath.Join(destDir, paths.ReposDir, e.Name(), "pkgs"))
+
+ packagesPath := filepath.Join(
+ repoPath,
+ "packages",
+ )
+
+ packages, err := os.ReadDir(packagesPath)
if err != nil {
continue
}
- pkgs, ok := uzbekdb.Loads(string(data), "map").(map[interface{}][]interface{})
- if !ok {
- continue
- }
- for name, val := range pkgs {
- found = append(found, Package{fmt.Sprintf("%v", name), e.Name(), val})
+
+ 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
- var packages = GetPackages(destDir)
- var splitName = false
- var repo string
+ name = strings.ToLower(name)
+
+ var repo string
if strings.Contains(name, "/") {
- splitName = true
parts := strings.SplitN(name, "/", 2)
repo = parts[0]
name = parts[1]
}
- for _, e := range packages {
- if splitName {
- if e.Name == name && e.Repo == repo {
- found = append(found, e)
- }
- } else {
- if e.Name == name {
- found = append(found, e)
- }
+ 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) {
- var pkgs []Package
- var pkg Package
+ name = strings.ToLower(name)
+
+ var repo, pkgName string
if strings.Contains(name, "/") {
parts := strings.SplitN(name, "/", 2)
- repo := parts[0]
- name = parts[1]
- pkgs = FindPackages(repo+"/"+name, destDir)
- if len(pkgs) == 0 {
- return nil, errors.New("no packages found with name " + name + " in " + repo + " repository")
- } else if len(pkgs) == 1 {
- pkg = pkgs[0]
- } else {
- for _, pkg := range pkgs {
- fmt.Printf("%s/%s - %v - %v\n", pkg.Repo, pkg.Name, pkg.Data[0], pkg.Data[1])
+ 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 {
+ 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
}
- return nil, errors.New("? found multiple packages with name " + name + " in " + repo + " repository")
+ _, err = io.Copy(tw, f)
+ f.Close()
+ return err
}
- } else {
- pkgs = FindPackages(name, destDir)
- if len(pkgs) == 0 {
- return nil, errors.New("no packages found with name " + name + " in repositories")
- } else if len(pkgs) > 1 {
- for _, pkg := range pkgs {
- fmt.Printf("%s/%s - %v - %v\n", pkg.Repo, pkg.Name, pkg.Data[0], pkg.Data[1])
+ })
+}
+
+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
}
- return nil, errors.New("found multiple packages with name " + name + " in repositories")
- } else {
- pkg = pkgs[0]
+ 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")
+ }
+ }
+
+ payloadDir := filepath.Join(stagingDir, "payload")
+ if _, err := os.Stat(payloadDir); err != nil {
+ return errors.New(
+ "qpmp has no payload",
+ )
+ }
+
+ 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
}
- return &pkg, nil
+ 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 qpmp payload: " + rel,
+ )
+ }
+ _, err = outputFile.WriteString(
+ "/"+filepath.ToSlash(rel)+"\n",
+ )
+ return err
+ },
+ )
+ if err != nil {
+ return err
+ }
+ outputFile.Close()
+
+ err = database.RegPackage(
+ key,
+ pkg.Version,
+ destDir,
+ )
+ if err != nil {
+ return err
+ }
+
+ fmt.Println(
+ ":: " + key +
+ " (" + pkg.Version + ") installed",
+ )
+
+ return nil
}
func Install(name string, args []string, destDir string) error {
- // verbose := slices.Contains(args, ":v")
+ if strings.HasSuffix(name, ".qpmp") {
+ if _, err := os.Stat(name); err == nil {
+ return installQpmp(name, args, destDir)
+ }
+ }
+
reinstall := slices.Contains(args, ":r")
disableDeps := slices.Contains(args, ":nd")
+ noLog := slices.Contains(args, ":nl")
+ force := slices.Contains(args, ":f")
+
pkg, err := getPackage(name, destDir)
if err != nil {
return err
}
- pkgPath := filepath.Join(destDir, paths.ReposDir, pkg.Repo, "packages", pkg.Name)
- depends, taip, url, afterScript, _ := readPackage(pkgPath)
+ key := pkg.Repo + "/" + pkg.Name
+
+ if _, ok := installing[key]; ok {
+ return nil
+ }
+
+ installing[key] = true
+ defer delete(installing, key)
- key := pkg.Repo + "/" + pkg.Name
+ if !force {
+ if matched, ok := conflictCheck(key, pkg.Conflicts, destDir); ok {
+ return errors.New(
+ key + " conflicts with " + matched,
+ )
+ }
+ }
- if installing[key] {
- return nil
- }
- installing[key] = true
- defer func() {
- installing[key] = false
- }()
-
if !disableDeps {
- if len(depends) != 0 {
- fmt.Println(helpers.ToString(len(depends)) + " deps for " + pkg.Repo + "/" + pkg.Name + ":")
- for i, n := range depends {
- if n == pkg.Name || n == pkg.Repo+"/"+pkg.Name {
- return errors.New("broken deps in package " + n)
+ 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,
+ )
}
- depPkgs := FindPackages(n, destDir)
- v := "not found"
+ depPkgs := FindPackages(dep, destDir)
+
+ version := "not found"
if len(depPkgs) > 0 {
- v = fmt.Sprintf("%v", depPkgs[0].Data[1])
+ version = depPkgs[0].Version
}
- fmt.Printf("%v. %s - %v\n", i+1, n, v)
+ fmt.Printf(
+ "%d. %s - %s\n",
+ i+1,
+ dep,
+ version,
+ )
}
}
}
-
+
if slices.Contains(args, ":a") {
- if !helpers.Ask("install " + pkg.Repo + "/" + pkg.Name + " with version " + helpers.ToString(pkg.Data[1])) {
+ if !helpers.Ask(
+ "install " +
+ key +
+ " with version " +
+ pkg.Version,
+ ) {
return errors.New("canceled by user")
}
}
-
+
if !disableDeps {
- for _, dep := range depends {
+ for _, dep := range pkg.Depends {
depPkg, err := getPackage(dep, destDir)
if err != nil {
return err
@@ -228,204 +1194,489 @@ func Install(name string, args []string, destDir string) error {
}
}
- fmt.Println(":: installing " + pkg.Name + " from repo " + pkg.Repo)
+ 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",
+ )
- if taip == "arch" {
- destArchivePath := filepath.Join(destDir, "tmp", pkg.Name + pkg.Repo + ".tar.zst")
- fullDestPath := filepath.Join(pkgPath, "arch")
+ fullDestPath := filepath.Join(
+ pkg.Path,
+ "extract",
+ )
- os.MkdirAll(fullDestPath, 0755)
os.MkdirAll(filepath.Dir(destArchivePath), 0755)
-
- client := &http.Client{
- Timeout: 10 * time.Second,
- }
+ defer os.Remove(destArchivePath)
+ defer os.RemoveAll(fullDestPath)
- resp, err := client.Get(url)
- if err != nil {
- fmt.Println("!! failed to download", url, err)
- return err
- }
- defer resp.Body.Close()
+ client := helpers.HTTPClient()
- f, err := os.Create(destArchivePath)
- if err != nil {
- fmt.Println("!! failed to create file", err)
+ 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()
return err
}
- defer f.Close()
- _, err = io.Copy(f, resp.Body)
- f.Close()
+ err := download()
if err != nil {
- fmt.Println("!! failed to copy", err)
- return err
+ 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
+ }
}
- fmt.Println(":: downloaded", url, "to", destArchivePath)
+ fmt.Println(":: copying files to /")
- _ = os.RemoveAll(fullDestPath)
-
- file, err := os.Open(destArchivePath)
+ err = installPayload(fullDestPath, destDir, pkg, !noLog)
if err != nil {
- fmt.Println("!! failed to open archive file", destArchivePath, err)
return err
}
- defer file.Close()
-
- err = extract.Zstd(context.TODO(), file, fullDestPath, nil)
+ } 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()
if err != nil {
- fmt.Println("!! failed to extract", err)
return err
}
- fmt.Println(":: removing govnofiles from package...")
+ } else if pkg.Type == "binary" {
+ outPath := filepath.Join(
+ destDir,
+ "usr/bin",
+ pkg.Name,
+ )
+
+ os.MkdirAll(filepath.Dir(outPath), 0755)
- _ = os.Remove(filepath.Join(fullDestPath, ".BUILDINFO"))
- _ = os.Remove(filepath.Join(fullDestPath, ".MTREE"))
- _ = os.Remove(filepath.Join(fullDestPath, ".PKGINFO"))
- _ = os.Remove(filepath.Join(fullDestPath, ".INSTALL"))
+ client := helpers.HTTPClient()
- fmt.Println(":: copying files to / ...")
-
- err = helpers.CopyDir(fullDestPath, filepath.Join(destDir, "/"))
+ resp, err := client.Get(pkg.URL)
+ downloadErr := error(nil)
if err != nil {
- fmt.Println("!! failed to copy files to /:", err)
+ downloadErr = err
+ } else if resp.StatusCode != http.StatusOK {
+ downloadErr = fmt.Errorf(
+ "download failed: HTTP %d",
+ resp.StatusCode,
+ )
} else {
- fmt.Println(":: successfully copied to /")
+ 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
+ }
+ }
}
-
- outputFilePath := filepath.Join(pkgPath, "files")
- outputFile, err := os.Create(outputFilePath)
- if err != nil {
- fmt.Println("!! failed to create files list:", err)
- return err
+ if resp != nil {
+ resp.Body.Close()
}
- defer outputFile.Close()
- err = filepath.WalkDir(fullDestPath, func(path string, d os.DirEntry, err error) error {
- if err != nil {
- return err
- }
-
- if d.IsDir() {
- return nil
+ 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,
+ )
}
+ defer os.RemoveAll(payloadDir)
- relPath, err := filepath.Rel(fullDestPath, path)
- if err != nil {
+ fmt.Println(":: copying files to /")
+
+ if err := installPayload(payloadDir, destDir, pkg, !noLog); err != nil {
return err
}
+ } else {
+ if !noLog {
+ outputFilePath := filepath.Join(
+ destDir,
+ paths.FilesDir,
+ pkg.Repo+"/"+pkg.Name+".files",
+ )
+ os.MkdirAll(filepath.Dir(outputFilePath), 0755)
- formattedPath := "/" + filepath.ToSlash(relPath)
+ outputFile, err := os.Create(outputFilePath)
+ if err != nil {
+ return err
+ }
+ defer outputFile.Close()
- _, err = outputFile.WriteString(formattedPath + "\n")
- return err
- })
+ _, err = outputFile.WriteString(
+ "/usr/bin/" + pkg.Name + "\n",
+ )
- if err != nil {
- fmt.Println("!! failed to index files:", err)
- return err
+ if err != nil {
+ return err
+ }
+ }
}
- } else if taip == "script" {
- cmd := exec.Command("sh", "install.sh")
- cmd.Dir = pkgPath
- cmd.Env = append(os.Environ(), "DESTDIR="+destDir)
+
+ } 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,
+ )
+
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
+
err := cmd.Run()
if err != nil {
return err
}
- } else if taip == "binary" {
- client := &http.Client{
- Timeout: 10 * time.Second,
- }
-
- resp, err := client.Get(url)
+ }
+
+
+ if !noLog {
+ err = database.RegPackage(
+ pkg.Repo+"/"+pkg.Name,
+ pkg.Version,
+ destDir,
+ )
+
if err != nil {
- return err
+ return errors.New(
+ "failed to register package: " +
+ err.Error(),
+ )
}
- defer resp.Body.Close()
-
- outPath := filepath.Join(destDir, "/usr/bin", pkg.Name)
-
- f, err := os.Create(outPath)
+ }
+
+ if pkg.Message != "" {
+ dataMsg, err := os.ReadFile(
+ filepath.Join(pkg.Path, pkg.Message),
+ )
if err != nil {
- return err
+ return errors.New(
+ "failed to read message from package: " +
+ err.Error(),
+ )
}
-
- _, err = io.Copy(f, resp.Body)
- f.Close()
- if err != nil {
- return err
+ 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
+ }
+ }
}
-
- err = os.Chmod(outPath, 0755)
- if err != nil {
- return err
+ }
+
+ 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",
+ )
}
- } else {
- return errors.New("package err maybe old format")
}
- if afterScript != "" {
- fmt.Println(":: running after installation script " + afterScript + " ...")
- cmd := exec.Command("sh", afterScript)
- cmd.Dir = pkgPath
- cmd.Env = append(os.Environ(), "DESTDIR="+destDir)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- err := cmd.Run()
+ 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,
+ )
if err != nil {
return err
}
}
- err = database.RegPackage(pkg.Repo+"/"+pkg.Name, helpers.ToString(pkg.Data[1]), destDir)
- if err != nil {
- return errors.New("failed to register package in installed database: " + err.Error())
- }
- fmt.Println(":: " + pkg.Repo + "/" + pkg.Name + " (" + helpers.ToString(pkg.Data[1]) + ") installed")
+ os.Remove(filesPath)
+
+ fmt.Println(
+ ":: " + key + " removed",
+ )
+
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)
+ }
return err
}
+ key := pkg.Repo + "/" + pkg.Name
+
if slices.Contains(args, ":a") {
- if !helpers.Ask("remove " + pkg.Repo + "/" + pkg.Name + " with repo version " + helpers.ToString(pkg.Data[1]) +
- " and local version " + database.GetPackageVersion(pkg.Repo+"/"+pkg.Name, destDir) + "?") {
- return errors.New("canceled by user")
+ localVersion := database.GetPackageVersion(
+ key,
+ destDir,
+ )
+
+ if !helpers.Ask(
+ "remove " + key +
+ " repo version " +
+ pkg.Version +
+ " local version " +
+ localVersion +
+ "?",
+ ) {
+ return errors.New(
+ "canceled by user",
+ )
}
}
- pkgPath := filepath.Join(destDir, paths.ReposDir, pkg.Repo, "packages", pkg.Name)
- _, _, _, _, removeScript := readPackage(pkgPath)
+ 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,
+ )
- if removeScript != "" {
- fmt.Println(":: running remove script " + removeScript + " ...")
- cmd := exec.Command("sh", removeScript)
- cmd.Dir = pkgPath
- cmd.Env = os.Environ()
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
- err = cmd.Run()
+
+ err := cmd.Run()
if err != nil {
return err
}
}
- if _, err := os.Stat("files"); err == nil {
- f, _ := os.Open("files")
+
+ 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
+ }
defer f.Close()
+
scanner := bufio.NewScanner(f)
+
for scanner.Scan() {
file := scanner.Text()
@@ -433,36 +1684,109 @@ func Remove(name string, args []string, destDir string) error {
continue
}
- path := filepath.Join(destDir, file)
+ 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.Printf("removing %s\n", path)
- if err := os.RemoveAll(path); err != nil {
- fmt.Fprintf(os.Stderr, "failed to remove %s: %v\n", path, err)
+ fmt.Println(
+ "removing",
+ path,
+ )
+
+ err := os.RemoveAll(path)
+ if err != nil {
+ fmt.Fprintf(
+ os.Stderr,
+ "failed removing %s: %v\n",
+ path,
+ err,
+ )
}
- } else if !os.IsNotExist(err) {
- fmt.Fprintf(os.Stderr, "failed to stat %s: %v\n", path, err)
}
}
+
if err := scanner.Err(); err != nil {
- fmt.Fprintf(os.Stderr, "failed to read files: %v\n", err)
+ return err
}
+
+ } else if pkg.Remove == "" {
+ return errors.New(
+ "package has no files list and no remove script",
+ )
}
- if removeScript == "" {
- if _, err := os.Stat("files"); os.IsNotExist(err) {
- return errors.New("failed to remove package file (no remove script and paths file)")
+
+ if !slices.Contains(args, ":nl") {
+ err = database.UnRegPackage(
+ key,
+ destDir,
+ )
+
+ if err != nil {
+ return err
}
}
-
- err = database.UnRegPackage(pkg.Repo+"/"+pkg.Name, destDir)
- if err != nil {
- return err
+
+ 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")
}
- v := helpers.ToString(pkg.Data[1])
- fmt.Println(":: " + pkg.Repo + "/" + pkg.Name + " (" + v + ") removed")
return nil
}