mirror of
https://github.com/EnumeratedDev/bpm.git
synced 2026-09-16 02:26:12 +00:00
1126 lines
30 KiB
Go
1126 lines
30 KiB
Go
package bpmlib
|
|
|
|
import (
|
|
"archive/tar"
|
|
"compress/gzip"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"regexp"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type BPMPackage struct {
|
|
PkgInfo *PackageInfo
|
|
PkgFiles []*PackageFileEntry
|
|
}
|
|
|
|
type PackageInfo struct {
|
|
Name string `yaml:"name"`
|
|
Description string `yaml:"description,omitempty"`
|
|
Version string `yaml:"version,omitempty"`
|
|
Revision int `yaml:"revision,omitempty"`
|
|
Url string `yaml:"url,omitempty"`
|
|
License string `yaml:"license,omitempty"`
|
|
Arch string `yaml:"architecture,omitempty"`
|
|
OutputArch string `yaml:"output_architecture,omitempty"`
|
|
Type string `yaml:"type,omitempty"`
|
|
Keep []string `yaml:"keep,omitempty"`
|
|
Depends []string `yaml:"depends,omitempty"`
|
|
RuntimeDepends []string `yaml:"runtime_depends,omitempty"`
|
|
OptionalDepends []string `yaml:"optional_depends,omitempty"`
|
|
MakeDepends []string `yaml:"make_depends,omitempty"`
|
|
Conflicts []string `yaml:"conflicts,omitempty"`
|
|
Replaces []string `yaml:"replaces,omitempty"`
|
|
Provides []string `yaml:"provides,omitempty"`
|
|
Options []string `yaml:"options,omitempty"`
|
|
Downloads []PackageDownload `yaml:"downloads,omitempty"`
|
|
SplitPackages []*PackageInfo `yaml:"split_packages,omitempty"`
|
|
}
|
|
|
|
type PackageDownload struct {
|
|
Url string `yaml:"url"`
|
|
Type string `yaml:"type,omitempty"`
|
|
Filepath string `yaml:"filepath,omitempty"`
|
|
|
|
// Archive options
|
|
NoExtract bool `yaml:"no_extract,omitempty"`
|
|
ExtractTo string `yaml:"extract_to,omitempty"`
|
|
ExtractStripComponents int `yaml:"extract_strip_components,omitempty"`
|
|
|
|
// Git options
|
|
CloneTo string `yaml:"clone_to,omitempty"`
|
|
GitBranch string `yaml:"git_branch,omitempty"`
|
|
|
|
Checksum string `yaml:"checksum,omitempty"`
|
|
}
|
|
|
|
type PackageFileEntry struct {
|
|
Path string
|
|
OctalPerms uint32
|
|
UserID int
|
|
GroupID int
|
|
SizeInBytes int64
|
|
}
|
|
|
|
func (pkg *BPMPackage) GetInstalledSize() int64 {
|
|
var totalSize int64 = 0
|
|
for _, entry := range pkg.PkgFiles {
|
|
totalSize += entry.SizeInBytes
|
|
}
|
|
return totalSize
|
|
}
|
|
|
|
func (pkg *BPMPackage) ConvertFilesToString() string {
|
|
str := ""
|
|
for _, file := range pkg.PkgFiles {
|
|
str += fmt.Sprintf("%s %d %d %d\n", file.Path, file.UserID, file.GroupID, file.SizeInBytes)
|
|
}
|
|
return str
|
|
}
|
|
|
|
func (pkgInfo *PackageInfo) GetFullVersion() string {
|
|
return pkgInfo.Version + "-" + strconv.Itoa(pkgInfo.Revision)
|
|
}
|
|
|
|
func (pkgInfo *PackageInfo) IsSplitPackage() bool {
|
|
// Return false if not a source package
|
|
if pkgInfo.Type != "source" {
|
|
return false
|
|
}
|
|
|
|
return len(pkgInfo.SplitPackages) > 0
|
|
}
|
|
|
|
func (pkgInfo *PackageInfo) GetSplitPackageInfo(splitPkg string) *PackageInfo {
|
|
for _, splitPkgInfo := range pkgInfo.SplitPackages {
|
|
if splitPkgInfo.Name == splitPkg {
|
|
return splitPkgInfo
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type InstallationReason string
|
|
|
|
const (
|
|
InstallationReasonManual InstallationReason = "manual"
|
|
InstallationReasonDependency InstallationReason = "dependency"
|
|
InstallationReasonMakeDependency InstallationReason = "make_dependency"
|
|
InstallationReasonUnknown InstallationReason = "unknown"
|
|
)
|
|
|
|
func GetInstallationReason(pkg, rootDir string) InstallationReason {
|
|
installedDir := path.Join(rootDir, "var/lib/bpm/installed/")
|
|
pkgDir := path.Join(installedDir, pkg)
|
|
if stat, err := os.Stat(path.Join(pkgDir, "installation_reason")); err != nil || stat.IsDir() {
|
|
return InstallationReasonManual
|
|
}
|
|
b, err := os.ReadFile(path.Join(pkgDir, "installation_reason"))
|
|
if err != nil {
|
|
return InstallationReasonUnknown
|
|
}
|
|
reason := strings.TrimSpace(string(b))
|
|
if reason == "manual" {
|
|
return InstallationReasonManual
|
|
} else if reason == "dependency" {
|
|
return InstallationReasonDependency
|
|
} else if reason == "make_dependency" {
|
|
return InstallationReasonMakeDependency
|
|
}
|
|
return InstallationReasonUnknown
|
|
}
|
|
|
|
func SetInstallationReason(pkg string, reason InstallationReason, rootDir string) error {
|
|
installedDir := path.Join(rootDir, "var/lib/bpm/installed/")
|
|
pkgDir := path.Join(installedDir, pkg)
|
|
err := os.WriteFile(path.Join(pkgDir, "installation_reason"), []byte(reason), 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GetPackageInfoRaw(filename string) (string, error) {
|
|
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
|
return "", err
|
|
}
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
tr := tar.NewReader(file)
|
|
for {
|
|
header, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if header.Name == "pkg.info" {
|
|
bs, _ := io.ReadAll(tr)
|
|
err := file.Close()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(bs), nil
|
|
}
|
|
}
|
|
return "", errors.New("pkg.info not found in archive")
|
|
}
|
|
|
|
func ReadPackage(filename string) (*BPMPackage, error) {
|
|
var pkgInfo *PackageInfo
|
|
var pkgFiles []*PackageFileEntry
|
|
|
|
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
|
return nil, err
|
|
}
|
|
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
tr := tar.NewReader(file)
|
|
for {
|
|
header, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if header.Name == "pkg.info" {
|
|
bs, _ := io.ReadAll(tr)
|
|
pkgInfo, err = ReadPackageInfo(string(bs))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if header.Name == "pkg.files" {
|
|
bs, _ := io.ReadAll(tr)
|
|
for _, line := range strings.Split(string(bs), "\n") {
|
|
if strings.TrimSpace(line) == "" {
|
|
continue
|
|
}
|
|
stringEntry := strings.Split(strings.TrimSpace(line), " ")
|
|
if len(stringEntry) < 5 {
|
|
return nil, errors.New("pkg.files is not formatted correctly")
|
|
}
|
|
octalPerms, err := strconv.ParseUint(stringEntry[len(stringEntry)-4], 8, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
uid, err := strconv.ParseInt(stringEntry[len(stringEntry)-3], 0, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
gid, err := strconv.ParseInt(stringEntry[len(stringEntry)-2], 0, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
size, err := strconv.ParseInt(stringEntry[len(stringEntry)-1], 0, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pkgFiles = append(pkgFiles, &PackageFileEntry{
|
|
Path: strings.Join(stringEntry[:len(stringEntry)-4], " "),
|
|
OctalPerms: uint32(octalPerms),
|
|
UserID: int(uid),
|
|
GroupID: int(gid),
|
|
SizeInBytes: size,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if pkgInfo == nil {
|
|
return nil, errors.New("pkg.info not found in archive")
|
|
}
|
|
return &BPMPackage{
|
|
PkgInfo: pkgInfo,
|
|
PkgFiles: pkgFiles,
|
|
}, nil
|
|
}
|
|
|
|
func getPackageScripts(filename string) (packageScripts []string) {
|
|
content, err := listTarballContent(filename)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, file := range content {
|
|
if file == "pre_install.sh" {
|
|
packageScripts = append(packageScripts, "pre_install.sh")
|
|
} else if file == "post_install.sh" {
|
|
packageScripts = append(packageScripts, "post_install.sh")
|
|
} else if file == "pre_update.sh" {
|
|
packageScripts = append(packageScripts, "pre_update.sh")
|
|
} else if file == "post_update.sh" {
|
|
packageScripts = append(packageScripts, "post_update.sh")
|
|
} else if file == "pre_remove.sh" {
|
|
packageScripts = append(packageScripts, "pre_remove.sh")
|
|
} else if file == "post_remove.sh" {
|
|
packageScripts = append(packageScripts, "post_remove.sh")
|
|
}
|
|
}
|
|
|
|
return packageScripts
|
|
}
|
|
|
|
func ReadPackageScripts(filename string) (map[string]string, error) {
|
|
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
|
return nil, err
|
|
}
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tr := tar.NewReader(file)
|
|
ret := make(map[string]string)
|
|
for {
|
|
header, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if header.Name == "pre_install.sh" || header.Name == "post_install.sh" || header.Name == "pre_update.sh" || header.Name == "post_update.sh" || header.Name == "pre_remove.sh" || header.Name == "post_remove.sh" {
|
|
bs, err := io.ReadAll(tr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret[header.Name] = string(bs)
|
|
}
|
|
}
|
|
|
|
err = file.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func executePackageScript(pkg, rootDir string, verbose bool, packageScript string) error {
|
|
var bpmpkg *BPMPackage
|
|
var err error
|
|
scripts := make(map[string]string)
|
|
|
|
// Fetch bpmpkg variable from file or installed package
|
|
if strings.HasSuffix(pkg, ".bpm") {
|
|
bpmpkg, err = ReadPackage(pkg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Read package scripts from tarball
|
|
scripts, err = ReadPackageScripts(pkg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
bpmpkg = GetPackage(pkg, rootDir)
|
|
if bpmpkg == nil {
|
|
return fmt.Errorf("Package not found: %s", pkg)
|
|
}
|
|
}
|
|
|
|
// Read installed remove package scripts
|
|
if IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) {
|
|
pkgDir := path.Join(rootDir, "var/lib/bpm/installed", bpmpkg.PkgInfo.Name)
|
|
if _, err := os.Stat(path.Join(pkgDir, "pre_remove.sh")); err == nil {
|
|
data, err := os.ReadFile(path.Join(pkgDir, "pre_remove.sh"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
scripts["pre_remove.sh"] = string(data)
|
|
}
|
|
if _, err := os.Stat(path.Join(pkgDir, "post_remove.sh")); err == nil {
|
|
data, err := os.ReadFile(path.Join(pkgDir, "post_remove.sh"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
scripts["post_remove.sh"] = string(data)
|
|
}
|
|
}
|
|
|
|
// Ensure package script exists
|
|
content, ok := scripts[packageScript]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
cmd := exec.Command("/bin/bash", "-c", content)
|
|
// Setup subprocess environment
|
|
cmd.Dir = "/"
|
|
// Run package script in chroot if using the -R flag
|
|
if rootDir != "/" {
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{Chroot: rootDir}
|
|
|
|
// Bind mount /etc/resolv.conf
|
|
mounted, err := func() (bool, error) {
|
|
if _, err := os.Stat("/etc/resolv.conf"); err != nil {
|
|
return false, nil
|
|
}
|
|
|
|
if _, err := os.Stat(path.Join(rootDir, "etc/resolv.conf")); os.IsNotExist(err) {
|
|
err = os.WriteFile(path.Join(rootDir, "/etc/resolv.conf"), nil, 0644)
|
|
if err != nil {
|
|
return false, nil
|
|
}
|
|
} else if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
mntCmd := exec.Command("mount", "-o", "ro,bind", "/etc/resolv.conf", path.Join(rootDir, "/etc/resolv.conf"))
|
|
if verbose {
|
|
mntCmd.Stdout = os.Stdout
|
|
mntCmd.Stderr = os.Stderr
|
|
}
|
|
err = mntCmd.Run()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return true, nil
|
|
}()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if mounted {
|
|
defer exec.Command("umount", "-f", path.Join(rootDir, "/etc/resolv.conf")).Run()
|
|
}
|
|
}
|
|
// Show output if verbose
|
|
if verbose {
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
}
|
|
// Setup command environment
|
|
cmd.Env = os.Environ()
|
|
cmd.Env = append(cmd.Env, "BPM_ROOT=/") // Setting to "/" for backwards compatibility
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_NAME=%s", bpmpkg.PkgInfo.Name))
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_DESC=%s", bpmpkg.PkgInfo.Description))
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_VERSION=%s", bpmpkg.PkgInfo.Version))
|
|
if IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) {
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_OLD_VERSION=%s", GetPackageInfo(bpmpkg.PkgInfo.Name, rootDir).Version))
|
|
}
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_REVISION=%d", bpmpkg.PkgInfo.Revision))
|
|
if IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) {
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_OLD_REVISION=%d", GetPackageInfo(bpmpkg.PkgInfo.Name, rootDir).Revision))
|
|
}
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_URL=%s", bpmpkg.PkgInfo.Url))
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_ARCH=%s", bpmpkg.PkgInfo.Arch))
|
|
depends := make([]string, len(bpmpkg.PkgInfo.Depends))
|
|
copy(depends, bpmpkg.PkgInfo.Depends)
|
|
for i := 0; i < len(depends); i++ {
|
|
depends[i] = fmt.Sprintf("\"%s\"", depends[i])
|
|
}
|
|
makeDepends := make([]string, len(bpmpkg.PkgInfo.MakeDepends))
|
|
copy(makeDepends, bpmpkg.PkgInfo.MakeDepends)
|
|
for i := 0; i < len(makeDepends); i++ {
|
|
makeDepends[i] = fmt.Sprintf("\"%s\"", makeDepends[i])
|
|
}
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_DEPENDS=(%s)", strings.Join(depends, " ")))
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_MAKE_DEPENDS=(%s)", strings.Join(makeDepends, " ")))
|
|
cmd.Env = append(cmd.Env, "BPM_PKG_TYPE=source")
|
|
// Run command
|
|
if verbose {
|
|
fmt.Printf("Running package script (%s) for package (%s)\n", packageScript, bpmpkg.PkgInfo.Name)
|
|
}
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
return PackageScriptErr{err: err, packageName: bpmpkg.PkgInfo.Name, packageScript: packageScript}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ReadPackageInfo(contents string) (*PackageInfo, error) {
|
|
pkgInfo := &PackageInfo{
|
|
Revision: 1,
|
|
OutputArch: GetArch(),
|
|
Keep: make([]string, 0),
|
|
Depends: make([]string, 0),
|
|
RuntimeDepends: make([]string, 0),
|
|
MakeDepends: make([]string, 0),
|
|
OptionalDepends: make([]string, 0),
|
|
Conflicts: make([]string, 0),
|
|
Replaces: make([]string, 0),
|
|
Provides: make([]string, 0),
|
|
Options: make([]string, 0),
|
|
Downloads: make([]PackageDownload, 0),
|
|
SplitPackages: make([]*PackageInfo, 0),
|
|
}
|
|
err := yaml.Unmarshal([]byte(contents), &pkgInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Ensure required fields are set properly
|
|
if pkgInfo.Name == "" {
|
|
return nil, errors.New("this package contains no name")
|
|
} else if pkgInfo.Description == "" {
|
|
return nil, errors.New("this package contains no description")
|
|
} else if pkgInfo.Version == "" {
|
|
return nil, errors.New("this package contains no version")
|
|
} else if pkgInfo.Revision <= 0 {
|
|
return nil, errors.New("this package contains a revision number less or equal to 0")
|
|
} else if pkgInfo.Arch == "" {
|
|
return nil, errors.New("this package contains no architecture")
|
|
} else if pkgInfo.Type == "" {
|
|
return nil, errors.New("this package contains no type")
|
|
}
|
|
for _, val := range pkgInfo.Keep {
|
|
if strings.HasPrefix(val, "/") {
|
|
return nil, fmt.Errorf("cannot keep file (%s) after update because it starts with a slash", val)
|
|
}
|
|
}
|
|
|
|
// Ensure package name is valid
|
|
if match, _ := regexp.MatchString("^[a-zA-Z0-9._-]+$", pkgInfo.Name); !match {
|
|
return nil, fmt.Errorf("package name (%s) is invalid", pkgInfo.Name)
|
|
}
|
|
|
|
// Setup split package information
|
|
for i, splitPkg := range pkgInfo.SplitPackages {
|
|
// Ensure split package contains a name
|
|
if splitPkg.Name == "" {
|
|
return nil, fmt.Errorf("package name (%s) is invalid", splitPkg.Name)
|
|
}
|
|
|
|
// Ensure split package name is valid
|
|
if match, _ := regexp.MatchString("^[a-zA-Z0-9._-]+$", splitPkg.Name); !match {
|
|
return nil, fmt.Errorf("package name (%s) is invalid", splitPkg.Name)
|
|
}
|
|
|
|
// Turn split package into yaml data
|
|
splitPkgYaml, err := yaml.Marshal(splitPkg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Clone all main package fields onto split package
|
|
*splitPkg = *pkgInfo
|
|
|
|
// Set split package field of split package to nil
|
|
splitPkg.SplitPackages = nil
|
|
|
|
// Unmarshal json data back to struct
|
|
err = yaml.Unmarshal(splitPkgYaml, splitPkg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Force set split package version, revision
|
|
splitPkg.Version = pkgInfo.Version
|
|
splitPkg.Revision = pkgInfo.Revision
|
|
|
|
pkgInfo.SplitPackages[i] = splitPkg
|
|
}
|
|
|
|
return pkgInfo, nil
|
|
}
|
|
|
|
func (pkgInfo *PackageInfo) CreateReadableInfo(rootDir string) string {
|
|
ret := make([]string, 0)
|
|
appendArray := func(label string, array []string) {
|
|
if len(array) == 0 {
|
|
return
|
|
}
|
|
|
|
ret = append(ret, fmt.Sprintf("%s: %s", label, strings.Join(array, ", ")))
|
|
}
|
|
|
|
ret = append(ret, "Name: "+pkgInfo.Name)
|
|
ret = append(ret, "Description: "+pkgInfo.Description)
|
|
ret = append(ret, "Version: "+pkgInfo.GetFullVersion())
|
|
if pkgInfo.Url != "" {
|
|
ret = append(ret, "URL: "+pkgInfo.Url)
|
|
}
|
|
if pkgInfo.License != "" {
|
|
ret = append(ret, "License: "+pkgInfo.License)
|
|
}
|
|
ret = append(ret, "Architecture: "+pkgInfo.Arch)
|
|
if pkgInfo.Type == "source" && pkgInfo.OutputArch != "" && pkgInfo.OutputArch != GetArch() {
|
|
ret = append(ret, "Output architecture: "+pkgInfo.Arch)
|
|
}
|
|
ret = append(ret, "Type: "+pkgInfo.Type)
|
|
appendArray("Dependencies", pkgInfo.Depends)
|
|
if pkgInfo.Type == "source" {
|
|
appendArray("Runtime Dependencies", pkgInfo.RuntimeDepends)
|
|
appendArray("Make Dependencies", pkgInfo.MakeDepends)
|
|
}
|
|
appendArray("Runtime dependencies", pkgInfo.RuntimeDepends)
|
|
appendArray("Optional dependencies", pkgInfo.OptionalDepends)
|
|
dependants := pkgInfo.GetPackageDependants(rootDir)
|
|
if len(dependants) > 0 {
|
|
appendArray("Dependant packages", dependants)
|
|
}
|
|
optionalDependants := pkgInfo.GetPackageOptionalDependants(rootDir)
|
|
if len(optionalDependants) > 0 {
|
|
appendArray("Optionally dependant packages", optionalDependants)
|
|
}
|
|
appendArray("Conflicting packages", pkgInfo.Conflicts)
|
|
appendArray("Provided packages", pkgInfo.Provides)
|
|
appendArray("Replaces packages", pkgInfo.Replaces)
|
|
|
|
if pkgInfo.Type == "source" && len(pkgInfo.SplitPackages) != 0 {
|
|
splitPkgs := make([]string, len(pkgInfo.SplitPackages))
|
|
for i, splitPkgInfo := range pkgInfo.SplitPackages {
|
|
splitPkgs[i] = splitPkgInfo.Name
|
|
}
|
|
appendArray("Split Packages", splitPkgs)
|
|
}
|
|
|
|
if rootDir != "" && IsPackageInstalled(pkgInfo.Name, rootDir) {
|
|
installationReason := GetInstallationReason(pkgInfo.Name, rootDir)
|
|
var installationReasonString string
|
|
switch installationReason {
|
|
case InstallationReasonManual:
|
|
installationReasonString = "Manual"
|
|
case InstallationReasonDependency:
|
|
installationReasonString = "Dependency"
|
|
case InstallationReasonMakeDependency:
|
|
installationReasonString = "Make dependency"
|
|
default:
|
|
installationReasonString = "Unknown"
|
|
}
|
|
ret = append(ret, "Installation Reason: "+installationReasonString)
|
|
}
|
|
|
|
return strings.Join(ret, "\n")
|
|
}
|
|
|
|
func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string) error {
|
|
seenHardlinks := make(map[string]string)
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Initialize progress bar
|
|
bar := createProgressBar(bpmpkg.GetInstalledSize(), "Installing "+bpmpkg.PkgInfo.Name, verbose)
|
|
defer bar.Close()
|
|
|
|
tarballFile, err := readTarballFile(filename, "files.tar.gz")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tarballFile.file.Close()
|
|
|
|
archive, err := gzip.NewReader(tarballFile.tarReader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
packageFilesReader := tar.NewReader(archive)
|
|
|
|
for {
|
|
header, err := packageFilesReader.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
extractFilename := path.Join(rootDir, header.Name)
|
|
switch header.Typeflag {
|
|
case tar.TypeDir:
|
|
if _, err := os.Stat(extractFilename); err == nil {
|
|
if verbose {
|
|
fmt.Printf("Skipping Directory: %s (Directory already exists)\n", extractFilename)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if err := os.Mkdir(extractFilename, 0755); err != nil && !os.IsExist(err) {
|
|
return err
|
|
}
|
|
|
|
err = os.Chown(extractFilename, header.Uid, header.Gid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Using syscall instead of os.Chmod because it seems to strip the setuid, setgid and sticky bits
|
|
err := syscall.Chmod(extractFilename, uint32(header.Mode))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if verbose {
|
|
fmt.Printf("Created directory %s (%o)\n", extractFilename, header.Mode)
|
|
}
|
|
bar.Add64(header.Size)
|
|
case tar.TypeReg:
|
|
skip := false
|
|
if _, err := os.Stat(extractFilename); err == nil {
|
|
for _, k := range bpmpkg.PkgInfo.Keep {
|
|
if strings.HasSuffix(k, "/") {
|
|
if strings.HasPrefix(header.Name, k) {
|
|
if verbose {
|
|
fmt.Printf("Skipping File: %s (Containing directory is set to be kept during reinstalls/updates)\n", extractFilename)
|
|
}
|
|
skip = true
|
|
continue
|
|
}
|
|
} else {
|
|
if header.Name == k {
|
|
if verbose {
|
|
fmt.Printf("Skipping File: %s (File is configured to be kept during reinstalls/updates)\n", extractFilename)
|
|
}
|
|
skip = true
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if skip {
|
|
continue
|
|
}
|
|
err := os.Remove(extractFilename)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
outFile, err := os.Create(extractFilename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.Copy(outFile, packageFilesReader); err != nil {
|
|
return err
|
|
}
|
|
err = outFile.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = os.Chown(extractFilename, header.Uid, header.Gid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Using syscall instead of os.Chmod because it seems to strip the setuid, setgid and sticky bits
|
|
err = syscall.Chmod(extractFilename, uint32(header.Mode))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if verbose {
|
|
fmt.Printf("Created File: %s (%o)\n", extractFilename, header.Mode)
|
|
}
|
|
bar.Add64(header.Size)
|
|
case tar.TypeSymlink:
|
|
err := os.Remove(extractFilename)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
err = os.Symlink(header.Linkname, extractFilename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if verbose {
|
|
fmt.Println("Created Symlink: " + extractFilename + " -> " + header.Linkname)
|
|
}
|
|
bar.Add64(header.Size)
|
|
case tar.TypeLink:
|
|
if verbose {
|
|
fmt.Println("Detected Hard Link: " + extractFilename + " -> " + path.Join(rootDir, strings.TrimPrefix(header.Linkname, "files/")))
|
|
}
|
|
seenHardlinks[extractFilename] = path.Join(strings.TrimPrefix(header.Linkname, "files/"))
|
|
err := os.Remove(extractFilename)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
bar.Add64(header.Size)
|
|
default:
|
|
return errors.New("unknown type (" + strconv.Itoa(int(header.Typeflag)) + ") in " + extractFilename)
|
|
}
|
|
}
|
|
for extractFilename, destination := range seenHardlinks {
|
|
err := os.Link(path.Join(rootDir, destination), extractFilename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if verbose {
|
|
fmt.Println("Created Hard Link: " + extractFilename + " -> " + path.Join(rootDir, destination))
|
|
}
|
|
}
|
|
defer archive.Close()
|
|
defer file.Close()
|
|
return nil
|
|
}
|
|
|
|
func installPackage(filename, rootDir string, verbose, force bool) error {
|
|
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
bpmpkg, err := ReadPackage(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ensure package type is 'binary'
|
|
if bpmpkg.PkgInfo.Type != "binary" {
|
|
return fmt.Errorf("can only extract binary packages")
|
|
}
|
|
|
|
packageInstalled := IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir)
|
|
|
|
// Run pre-* package scripts
|
|
if !packageInstalled {
|
|
err := executePackageScript(filename, rootDir, verbose, "pre_install.sh")
|
|
if err != nil {
|
|
log.Printf("Warning: %s\n", err)
|
|
}
|
|
} else {
|
|
err := executePackageScript(filename, rootDir, verbose, "pre_update.sh")
|
|
if err != nil {
|
|
log.Printf("Warning: %s\n", err)
|
|
}
|
|
}
|
|
|
|
// Check if package is installed and remove current files
|
|
if packageInstalled {
|
|
// Fetching and reversing package file entry list
|
|
fileEntries := GetPackage(bpmpkg.PkgInfo.Name, rootDir).PkgFiles
|
|
sort.Slice(fileEntries, func(i, j int) bool {
|
|
return fileEntries[i].Path < fileEntries[j].Path
|
|
})
|
|
slices.Reverse(fileEntries)
|
|
files, err := GetAllPackageFiles(rootDir, bpmpkg.PkgInfo.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Removing old package files
|
|
if verbose {
|
|
fmt.Printf("Removing old files for package (%s)...\n", bpmpkg.PkgInfo.Name)
|
|
}
|
|
for _, entry := range fileEntries {
|
|
file := path.Join(rootDir, entry.Path)
|
|
stat, err := os.Lstat(file)
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(files[entry.Path]) != 0 {
|
|
if verbose {
|
|
fmt.Println("Skipping path: " + file + " (Path is managed by multiple packages)")
|
|
}
|
|
continue
|
|
}
|
|
shouldContinue := false
|
|
for _, value := range bpmpkg.PkgInfo.Keep {
|
|
if strings.HasSuffix(value, "/") {
|
|
if strings.HasPrefix(entry.Path, value) || entry.Path == strings.TrimSuffix(value, "/") {
|
|
if verbose {
|
|
fmt.Println("Skipping path: " + file + " (Path is set to be kept during reinstalls/updates)")
|
|
}
|
|
shouldContinue = true
|
|
continue
|
|
}
|
|
} else {
|
|
if entry.Path == value {
|
|
if verbose {
|
|
fmt.Println("Skipping path: " + file + " (Path is set to be kept during reinstalls/updates)")
|
|
}
|
|
shouldContinue = true
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
if shouldContinue {
|
|
continue
|
|
}
|
|
if stat.Mode()&os.ModeSymlink != 0 {
|
|
if verbose {
|
|
fmt.Println("Removing: " + file)
|
|
}
|
|
err := os.Remove(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if stat.IsDir() {
|
|
dir, err := os.ReadDir(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(dir) != 0 {
|
|
if verbose {
|
|
fmt.Println("Skipping non-empty directory: " + file)
|
|
}
|
|
continue
|
|
}
|
|
if verbose {
|
|
fmt.Println("Removing: " + file)
|
|
}
|
|
err = os.Remove(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if verbose {
|
|
fmt.Println("Removing: " + file)
|
|
}
|
|
err := os.Remove(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !force {
|
|
if bpmpkg.PkgInfo.Arch != "any" && bpmpkg.PkgInfo.Arch != GetArch() {
|
|
return errors.New("cannot install a package with a different architecture")
|
|
}
|
|
}
|
|
|
|
if verbose {
|
|
fmt.Printf("Extracting files for package (%s)...\n", bpmpkg.PkgInfo.Name)
|
|
}
|
|
|
|
// Extract package files into rootDir
|
|
err = extractPackage(bpmpkg, verbose, filename, rootDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
installedDir := path.Join(rootDir, "var/lib/bpm/installed/")
|
|
err = os.MkdirAll(installedDir, 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pkgDir := path.Join(installedDir, bpmpkg.PkgInfo.Name)
|
|
|
|
err = os.MkdirAll(pkgDir, 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
f, err := os.Create(path.Join(pkgDir, "files"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tarballFile, err := readTarballFile(filename, "pkg.files")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tarballFile.file.Close()
|
|
|
|
_, err = io.Copy(f, tarballFile.tarReader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
f, err = os.Create(path.Join(pkgDir, "info"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
raw, err := GetPackageInfoRaw(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = f.WriteString(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = f.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Save remove package scripts
|
|
packageScripts, err := ReadPackageScripts(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for script, content := range packageScripts {
|
|
if !strings.HasSuffix(script, "_remove.sh") {
|
|
continue
|
|
}
|
|
|
|
// Create file
|
|
f, err = os.Create(path.Join(pkgDir, script))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write script contents to file
|
|
_, err = f.WriteString(content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Close file
|
|
f.Close()
|
|
}
|
|
|
|
// Run post-* package scripts
|
|
if !packageInstalled {
|
|
err = executePackageScript(filename, rootDir, verbose, "post_install.sh")
|
|
if err != nil {
|
|
log.Printf("Warning: %s\n", err)
|
|
}
|
|
} else {
|
|
err = executePackageScript(filename, rootDir, verbose, "post_update.sh")
|
|
if err != nil {
|
|
log.Printf("Warning: %s\n", err)
|
|
}
|
|
}
|
|
|
|
// Ensure local package information has been initialized for rootDir
|
|
err = initializeLocalPackageInformation(rootDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add or update package information for rootDir
|
|
localPackageInformation[rootDir][bpmpkg.PkgInfo.Name] = bpmpkg.PkgInfo
|
|
|
|
return nil
|
|
}
|
|
|
|
func removePackage(pkg string, verbose bool, rootDir string) error {
|
|
pkgDir := path.Join("/var/lib/bpm/installed/", pkg)
|
|
pkgInfo := GetPackageInfo(pkg, rootDir)
|
|
if pkgInfo == nil {
|
|
return errors.New("could not get package info")
|
|
}
|
|
|
|
// Executing pre_remove script
|
|
err := executePackageScript(pkg, rootDir, verbose, "pre_remove.sh")
|
|
if err != nil {
|
|
log.Printf("Warning: %s\n", err)
|
|
}
|
|
|
|
// Get BPM package
|
|
bpmpkg := GetPackage(pkg, rootDir)
|
|
|
|
// Fetching and reversing package file entry list
|
|
fileEntries := bpmpkg.PkgFiles
|
|
sort.Slice(fileEntries, func(i, j int) bool {
|
|
return fileEntries[i].Path < fileEntries[j].Path
|
|
})
|
|
slices.Reverse(fileEntries)
|
|
files, err := GetAllPackageFiles(rootDir, pkg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bar := createProgressBar(bpmpkg.GetInstalledSize(), "Removing "+bpmpkg.PkgInfo.Name, verbose)
|
|
defer bar.Close()
|
|
|
|
// Removing package files
|
|
for _, entry := range fileEntries {
|
|
bar.Add64(entry.SizeInBytes)
|
|
file := path.Join(rootDir, entry.Path)
|
|
lstat, err := os.Lstat(file)
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(files[entry.Path]) != 0 {
|
|
if verbose {
|
|
fmt.Println("Skipping path: " + file + "(Path is managed by multiple packages)")
|
|
}
|
|
continue
|
|
}
|
|
if lstat.Mode()&os.ModeSymlink != 0 {
|
|
if verbose {
|
|
fmt.Println("Removing: " + file)
|
|
}
|
|
err := os.Remove(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
stat, err := os.Stat(file)
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if stat.IsDir() {
|
|
dir, err := os.ReadDir(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(dir) != 0 {
|
|
if verbose {
|
|
fmt.Println("Skipping non-empty directory: " + file)
|
|
}
|
|
continue
|
|
}
|
|
if verbose {
|
|
fmt.Println("Removing: " + file)
|
|
}
|
|
err = os.Remove(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if verbose {
|
|
fmt.Println("Removing: " + file)
|
|
}
|
|
err := os.Remove(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Executing post_remove script
|
|
err = executePackageScript(pkg, rootDir, verbose, "post_remove.sh")
|
|
if err != nil {
|
|
log.Printf("Warning: %s\n", err)
|
|
}
|
|
|
|
// Removing package directory
|
|
if verbose {
|
|
fmt.Println("Removing: " + path.Join(rootDir, pkgDir))
|
|
}
|
|
err = os.RemoveAll(path.Join(rootDir, pkgDir))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ensure local package information has been initialized for rootDir
|
|
err = initializeLocalPackageInformation(rootDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add or update package information for rootDir
|
|
delete(localPackageInformation[rootDir], pkgInfo.Name)
|
|
|
|
return nil
|
|
}
|