diff options
| author | huker667 <huker@tuta.io> | 2026-08-11 18:58:45 +0300 |
|---|---|---|
| committer | huker667 <huker@tuta.io> | 2026-08-11 18:58:45 +0300 |
| commit | d6ebd95cbdbd98b959e52122f2924621b510d291 (patch) | |
| tree | a8a176a2c7186d2a99b2c214a2f6a4aa9af95c0c | |
| parent | 22359129afeef78e0fcd56e62586819c06950846 (diff) | |
| download | qulay-main.tar.gz qulay-main.tar.bz2 qulay-main.zip | |
| -rw-r--r-- | config/paths.go | 5 | ||||
| -rw-r--r-- | database/database.go | 161 | ||||
| -rw-r--r-- | go.mod | 2 | ||||
| -rw-r--r-- | helpers/helpers.go | 17 | ||||
| -rw-r--r-- | main.go | 93 | ||||
| -rw-r--r-- | package/package.go | 1822 |
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 { -// -// } @@ -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(): @@ -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 } |