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 /package | |
| parent | 22359129afeef78e0fcd56e62586819c06950846 (diff) | |
| download | qulay-main.tar.gz qulay-main.tar.bz2 qulay-main.zip | |
Diffstat (limited to 'package')
| -rw-r--r-- | package/package.go | 1822 |
1 files changed, 1573 insertions, 249 deletions
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 } |