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