diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6bbca8c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# IDE Directories +.idea + +# Build directory +build diff --git a/Makefile b/Makefile index f9a3fd7..2bfb0de 100644 --- a/Makefile +++ b/Makefile @@ -1,38 +1,40 @@ -ifeq ($(PREFIX),) - PREFIX := /usr/local -endif -ifeq ($(BINDIR),) - BINDIR := $(PREFIX)/bin -endif -ifeq ($(SYSCONFDIR),) - SYSCONFDIR := $(PREFIX)/etc -endif -ifeq ($(GO),) - GO := $(shell type -a -P go | head -n 1) -endif +# Installation paths +PREFIX ?= /usr/local +SBINDIR ?= $(PREFIX)/sbin +SYSCONFDIR := $(PREFIX)/etc + +# Compilers and tools +GO ?= go + +# Build-time variables +ROOT_COMPILATION_UID ?= 65534 +ROOT_COMPILATION_GID ?= 65534 build: mkdir -p build - $(GO) build -ldflags "-w" -o build/bpm gitlab.com/bubble-package-manager/bpm + cd src/bpm; $(GO) build -ldflags "-w -X 'git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib.rootCompilationUID=$(ROOT_COMPILATION_UID)' -X 'git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib.rootCompilationGID=$(ROOT_COMPILATION_GID)'" -o ../../build/bpm git.enumerated.dev/bubble-package-manager/bpm/src/bpm install: build/bpm config/ - mkdir -p $(DESTDIR)$(BINDIR) - mkdir -p $(DESTDIR)$(SYSCONFDIR) - cp build/bpm $(DESTDIR)$(BINDIR)/bpm - cp config/bpm.conf $(DESTDIR)$(SYSCONFDIR)/bpm.conf + # Create directory + install -dm755 $(DESTDIR)$(SBINDIR) -compress: build/bpm config/ - mkdir -p bpm/$(BINDIR) - mkdir -p bpm/$(SYSCONFDIR) - cp build/bpm bpm/$(BINDIR)/bpm - cp config/bpm.conf bpm/$(SYSCONFDIR)/bpm.conf - tar --owner=root --group=root -czf bpm.tar.gz bpm - rm -r bpm + # Install binary + install -Dm755 build/bpm $(DESTDIR)$(SBINDIR)/bpm -run: build/bpm - build/bpm +install-config: + # Create directory + install -dm755 $(DESTDIR)$(SYSCONFDIR) + + # Install files + install -Dm644 config/bpm.conf $(DESTDIR)$(SYSCONFDIR)/bpm.conf + install -Dm644 config/bpm-compilation.conf $(DESTDIR)$(SYSCONFDIR)/bpm-compilation.conf + +uninstall: + -rm -f $(DESTDIR)$(SBINDIR)/bpm + -rm -f $(DESTDIR)$(SYSCONFDIR)/bpm.conf + -rm -f $(DESTDIR)$(SYSCONFDIR)/bpm-compilation.conf clean: rm -r build/ -.PHONY: build \ No newline at end of file +.PHONY: build install install-config uninstall clean diff --git a/README.md b/README.md index 637e170..7fa9a95 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,25 @@ # Bubble Package Manager (BPM) -## _A simple package manager_ - -BPM is a simple package manager for Linux systems +## _The official package manager for Tide Linux_ ## Features - Simple to use subcommands -- Can install binary and source packages -- Can be easily installed on practically any system -- No bloat +- Can install both binary and source packages ## Information - -BPM is still in very early development. It should not be installed on any system you use seriously. I recommend trying this out in a VM or container. In addition to this, this is one of the first projects I have made using the go programming language so code quality may not be the best. This project was made to help me learn go and how linux systems work better. It is not meant to replace the big package managers in any way +BPM is still in very early development. There may be bugs that could completely break installations. For now it is recommended you use this only in Virtual Machines, containers or in a Tide Linux installation ## Build from source - Download `go` from your package manager or from the go website - Download `make` from your package manager -- Run the following command to compile the project -``` +- Run the following command to compile the project. You may need to set the `GO` environment variable if your Go installation is not in your PATH +```sh make ``` -- Run the following command to install stormfetch into your system. You may also append a DESTDIR variable at the end of this line if you wish to install in a different location -``` +- Run the following command to install BPM into your system. You may also append a DESTDIR variable at the end of this line if you wish to install in a different location +```sh make install PREFIX=/usr SYSCONFDIR=/etc +make install-config PREFIX=/usr SYSCONFDIR=/etc ``` ## How to use @@ -32,24 +28,38 @@ You are able to install bpm packages by typing the following: ```sh bpm install /path/to/package.bpm ``` -You may also use the -y flag as shown below to bypass the installation confirmation prompt +You can also use the package name directly if using databases +```sh +bpm install package_name +``` +The -y flag may be used as shown below to bypass the confirmation prompt ```sh bpm install -y /path/to/package.bpm ``` -Flags must strictly be typed before the first package path otherwise they'll be read as package locations themselves +Flags must strictly be typed before the first package path or name, otherwise they'll be read as package locations themselves You can remove an installed package by typing the following ```sh bpm remove package_name ``` -The -y flag applies here as well if you wish to skip the removal confirmation prompt + +To remove all unused dependencies and clean cached files try using the cleanup command +```sh +bpm cleanup +``` + +If using databases, all packages can be updated using this simple command +```sh +bpm update +``` For information on the rest of the commands simply use the help command or pass in no arguments at all -``` +```sh bpm help ``` ## Package Creation -Package creation is simplified using the bpm-utils package which contains helper scripts for creating and archiving packages. \ -Learn more here: https://gitlab.com/bubble-package-manager/bpm-utils \ No newline at end of file +Package creation is simplified using the bpm-utils package which contains helper scripts for creating packages + +Learn more here: https://git.enumerated.dev/bubble-package-manager/bpm-utils diff --git a/config/bpm-compilation.conf b/config/bpm-compilation.conf new file mode 100644 index 0000000..cd28321 --- /dev/null +++ b/config/bpm-compilation.conf @@ -0,0 +1,2 @@ +privilege_escalator_cmd: "sudo" +compilation_env: [] diff --git a/config/bpm.conf b/config/bpm.conf index 6b92f57..8c64765 100644 --- a/config/bpm.conf +++ b/config/bpm.conf @@ -1,8 +1,6 @@ -compilation_env: [] -silent_compilation: false -compilation_dir: "/var/tmp/" -binary_output_dir: "/var/lib/bpm/compiled/" -repositories: - - name: example-repository - source: https://my-repo.xyz/ - disabled: true \ No newline at end of file +ignore_packages: [] +cleanup_make_dependencies: true +databases: + - name: example-database + source: https://my-database.xyz/ + disabled: true diff --git a/go.mod b/go.mod deleted file mode 100644 index eb5bc0a..0000000 --- a/go.mod +++ /dev/null @@ -1,9 +0,0 @@ -module gitlab.com/bubble-package-manager/bpm - -go 1.22 - -require ( - github.com/elliotchance/orderedmap/v2 v2.4.0 // indirect - github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/main.go b/main.go deleted file mode 100644 index c5b69d8..0000000 --- a/main.go +++ /dev/null @@ -1,599 +0,0 @@ -package main - -import ( - "bufio" - "flag" - "fmt" - "gitlab.com/bubble-package-manager/bpm/utils" - "log" - "os" - "path/filepath" - "slices" - "strings" -) - -/* -------------BPM | Bubble Package Manager-------------- */ -/* Made By EnumDev (Previously CapCreeperGR) */ -/* A simple-to-use package manager */ -/* ------------------------------------------------------- */ - -var bpmVer = "0.5.0" - -var subcommand = "help" -var subcommandArgs []string - -// Flags -var rootDir = "/" -var verbose = false -var yesAll = false -var buildSource = false -var skipCheck = false -var keepTempDir = false -var force = false -var pkgListNumbers = false -var pkgListNames = false -var reinstall = false -var reinstallAll = false -var noOptional = false -var nosync = true - -func main() { - utils.ReadConfig() - resolveFlags() - resolveCommand() -} - -type commandType uint8 - -const ( - _default commandType = iota - help - info - list - search - install - update - sync - remove - file -) - -func getCommandType() commandType { - switch subcommand { - case "version": - return _default - case "info": - return info - case "list": - return list - case "search": - return search - case "install": - return install - case "update": - return update - case "sync": - return sync - case "remove": - return remove - case "file": - return file - default: - return help - } -} - -func resolveCommand() { - switch getCommandType() { - case _default: - fmt.Println("Bubble Package Manager (BPM)") - fmt.Println("Version: " + bpmVer) - case info: - packages := subcommandArgs - if len(packages) == 0 { - fmt.Println("No packages were given") - return - } - for n, pkg := range packages { - var info *utils.PackageInfo - info = utils.GetPackageInfo(pkg, rootDir) - if info == nil { - log.Fatalf("Error: package (%s) is not installed\n", pkg) - } - fmt.Println("----------------") - fmt.Println(utils.CreateReadableInfo(true, true, true, info, rootDir)) - if n == len(packages)-1 { - fmt.Println("----------------") - } - } - case list: - packages, err := utils.GetInstalledPackages(rootDir) - if err != nil { - log.Fatalf("Error: could not get installed packages: %s", err.Error()) - return - } - if pkgListNumbers { - fmt.Println(len(packages)) - } else if pkgListNames { - for _, pkg := range packages { - fmt.Println(pkg) - } - } else { - if len(packages) == 0 { - fmt.Println("No packages have been installed") - return - } - for n, pkg := range packages { - info := utils.GetPackageInfo(pkg, rootDir) - if info == nil { - fmt.Printf("Package (%s) could not be found\n", pkg) - continue - } - fmt.Println("----------------\n" + utils.CreateReadableInfo(true, true, true, info, rootDir)) - if n == len(packages)-1 { - fmt.Println("----------------") - } - } - } - case search: - searchTerms := subcommandArgs - if len(searchTerms) == 0 { - log.Fatalf("Error: no search terms given") - } - - for _, term := range searchTerms { - nameResults := make([]*utils.PackageInfo, 0) - descResults := make([]*utils.PackageInfo, 0) - for _, repo := range utils.BPMConfig.Repositories { - for _, entry := range repo.Entries { - if strings.Contains(entry.Info.Name, term) { - nameResults = append(nameResults, entry.Info) - } else if strings.Contains(entry.Info.Description, term) { - descResults = append(descResults, entry.Info) - } - } - } - results := append(nameResults, descResults...) - if len(results) == 0 { - log.Fatalf("Error: no results for term (%s) were found\n", term) - } - fmt.Printf("Results for term (%s)\n", term) - for i, result := range results { - fmt.Println("----------------") - fmt.Printf("%d) %s: %s (%s)\n", i+1, result.Name, result.Description, result.GetFullVersion()) - } - } - case install: - if os.Getuid() != 0 { - log.Fatalf("Error: this subcommand needs to be run with superuser permissions") - } - pkgs := subcommandArgs - if len(pkgs) == 0 { - fmt.Println("No packages or files were given to install") - return - } - - operation := utils.BPMOperation{ - Actions: make([]utils.OperationAction, 0), - UnresolvedDepends: make([]string, 0), - RootDir: rootDir, - } - - // Search for packages - for _, pkg := range pkgs { - if stat, err := os.Stat(pkg); err == nil && !stat.IsDir() { - bpmpkg, err := utils.ReadPackage(pkg) - if err != nil { - log.Fatalf("Error: could not read package: %s\n", err) - } - if !reinstall && utils.IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) && utils.GetPackageInfo(bpmpkg.PkgInfo.Name, rootDir).GetFullVersion() == bpmpkg.PkgInfo.GetFullVersion() { - continue - } - operation.Actions = append(operation.Actions, &utils.InstallPackageAction{ - File: pkg, - IsDependency: false, - BpmPackage: bpmpkg, - }) - } else { - entry, _, err := utils.GetRepositoryEntry(pkg) - if err != nil { - log.Fatalf("Error: could not find package (%s) in any repository\n", pkg) - } - if !reinstall && utils.IsPackageInstalled(entry.Info.Name, rootDir) && utils.GetPackageInfo(entry.Info.Name, rootDir).GetFullVersion() == entry.Info.GetFullVersion() { - continue - } - operation.Actions = append(operation.Actions, &utils.FetchPackageAction{ - IsDependency: false, - RepositoryEntry: entry, - }) - } - } - - // Resolve dependencies - err := operation.ResolveDependencies(reinstallAll, !noOptional, verbose) - if err != nil { - log.Fatalf("Error: could not resolve dependencies: %s\n", err) - } - if len(operation.UnresolvedDepends) != 0 { - if !force { - log.Fatalf("Error: the following dependencies could not be found in any repositories: %s\n", strings.Join(operation.UnresolvedDepends, ", ")) - } else { - log.Println("Warning: The following dependencies could not be found in any repositories: " + strings.Join(operation.UnresolvedDepends, ", ")) - } - } - - // Show operation summary - operation.ShowOperationSummary() - - // Confirmation Prompt - if !yesAll { - reader := bufio.NewReader(os.Stdin) - if len(operation.Actions) == 1 { - fmt.Printf("Do you wish to install this package? [y\\N] ") - } else { - fmt.Printf("Do you wish to install these %d packages? [y\\N] ", len(operation.Actions)) - } - - text, _ := reader.ReadString('\n') - if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" { - fmt.Println("Cancelling package installation...") - os.Exit(1) - } - } - - // Execute operation - err = operation.Execute(verbose, force) - if err != nil { - log.Fatalf("Error: could not complete operation: %s\n", err) - } - case update: - if os.Getuid() != 0 { - log.Fatalf("Error: this subcommand needs to be run with superuser permissions") - } - - // Sync repositories - if !nosync { - for _, repo := range utils.BPMConfig.Repositories { - fmt.Printf("Fetching package database for repository (%s)...\n", repo.Name) - err := repo.SyncLocalDatabase() - if err != nil { - log.Fatalf("Error: could not sync local database for repository (%s): %s\n", repo.Name, err) - } - } - fmt.Println("All package databases synced successfully!") - } - - utils.ReadConfig() - - // Get installed packages and check for updates - pkgs, err := utils.GetInstalledPackages(rootDir) - if err != nil { - log.Fatalf("Error: could not get installed packages: %s\n", err) - } - - operation := utils.BPMOperation{ - Actions: make([]utils.OperationAction, 0), - UnresolvedDepends: make([]string, 0), - RootDir: rootDir, - } - - // Search for packages - for _, pkg := range pkgs { - entry, _, err := utils.GetRepositoryEntry(pkg) - if err != nil { - continue - } - installedInfo := utils.GetPackageInfo(pkg, rootDir) - if installedInfo == nil { - log.Fatalf("Error: could not get package info for (%s)\n", pkg) - } else { - comparison := utils.ComparePackageVersions(*entry.Info, *installedInfo) - if comparison > 0 || reinstall { - operation.Actions = append(operation.Actions, &utils.FetchPackageAction{ - IsDependency: false, - RepositoryEntry: entry, - }) - } - } - } - - // Check for new dependencies in updated packages - err = operation.ResolveDependencies(reinstallAll, !noOptional, verbose) - if err != nil { - log.Fatalf("Error: could not resolve dependencies: %s\n", err) - } - if len(operation.UnresolvedDepends) != 0 { - if !force { - log.Fatalf("Error: the following dependencies could not be found in any repositories: %s\n", strings.Join(operation.UnresolvedDepends, ", ")) - } else { - log.Println("Warning: The following dependencies could not be found in any repositories: " + strings.Join(operation.UnresolvedDepends, ", ")) - } - } - - // Show operation summary - operation.ShowOperationSummary() - - // Confirmation Prompt - if !yesAll { - fmt.Printf("Are you sure you wish to update all %d packages? [y\\N] ", len(operation.Actions)) - reader := bufio.NewReader(os.Stdin) - text, _ := reader.ReadString('\n') - if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" { - fmt.Println("Cancelling package update...") - os.Exit(1) - } - } - - // Execute operation - err = operation.Execute(verbose, force) - if err != nil { - log.Fatalf("Error: could not complete operation: %s\n", err) - } - case sync: - if os.Getuid() != 0 { - log.Fatalf("Error: this subcommand needs to be run with superuser permissions") - } - if !yesAll { - fmt.Printf("Are you sure you wish to sync all databases? [y\\N] ") - reader := bufio.NewReader(os.Stdin) - text, _ := reader.ReadString('\n') - if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" { - fmt.Println("Cancelling database synchronization...") - os.Exit(1) - } - } - for _, repo := range utils.BPMConfig.Repositories { - fmt.Printf("Fetching package database for repository (%s)...\n", repo.Name) - err := repo.SyncLocalDatabase() - if err != nil { - log.Fatalf("Error: could not sync local database for repository (%s): %s\n", repo.Name, err) - } - } - fmt.Println("All package databases synced successfully!") - case remove: - if os.Getuid() != 0 { - log.Fatalf("Error: this subcommand needs to be run with superuser permissions") - } - packages := subcommandArgs - if len(packages) == 0 { - fmt.Println("No packages were given") - return - } - - operation := &utils.BPMOperation{ - Actions: make([]utils.OperationAction, 0), - UnresolvedDepends: make([]string, 0), - RootDir: rootDir, - } - - // Search for packages - for _, pkg := range packages { - bpmpkg := utils.GetPackage(pkg, rootDir) - if bpmpkg == nil { - log.Fatalf("Error: package (%s) could not be found\n", pkg) - } - operation.Actions = append(operation.Actions, &utils.RemovePackageAction{BpmPackage: bpmpkg}) - } - - // Show operation summary - operation.ShowOperationSummary() - - // Confirmation Prompt - if !yesAll { - fmt.Printf("Are you sure you wish to remove all %d packages? [y\\N] ", len(operation.Actions)) - reader := bufio.NewReader(os.Stdin) - text, _ := reader.ReadString('\n') - if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" { - fmt.Println("Cancelling package removal...") - os.Exit(1) - } - } - - // Execute operation - err := operation.Execute(verbose, force) - if err != nil { - log.Fatalf("Error: could not complete operation: %s\n", err) - } - case file: - files := subcommandArgs - if len(files) == 0 { - fmt.Println("No files were given to get which packages manage it") - return - } - for _, file := range files { - absFile, err := filepath.Abs(file) - if err != nil { - log.Fatalf("Error: could not get absolute path of file (%s)\n", file) - } - stat, err := os.Stat(absFile) - if os.IsNotExist(err) { - log.Fatalf("Error: file (%s) does not exist!\n", absFile) - } - pkgs, err := utils.GetInstalledPackages(rootDir) - if err != nil { - log.Fatalf("Error: could not get installed packages: %s\n", err.Error()) - } - - if !strings.HasPrefix(absFile, rootDir) { - log.Fatalf("Error: could not get path of file (%s) relative to root path", absFile) - } - absFile, err = filepath.Rel(rootDir, absFile) - if err != nil { - log.Fatalf("Error: could not get path of file (%s) relative to root path", absFile) - } - absFile = strings.TrimPrefix(absFile, "/") - if stat.IsDir() { - absFile = absFile + "/" - } - - var pkgList []string - for _, pkg := range pkgs { - if slices.ContainsFunc(utils.GetPackageFiles(pkg, rootDir), func(entry *utils.PackageFileEntry) bool { - return entry.Path == absFile - }) { - pkgList = append(pkgList, pkg) - } - } - if len(pkgList) == 0 { - fmt.Println(absFile + " is not managed by any packages") - } else { - fmt.Println(absFile + " is managed by the following packages:") - for _, pkg := range pkgList { - fmt.Println("- " + pkg) - } - } - } - default: - printHelp() - } -} - -func printHelp() { - fmt.Println("\033[1m---- Command Format ----\033[0m") - fmt.Println("-> command format: bpm [-flags]...") - fmt.Println("-> flags will be read if passed right after the subcommand otherwise they will be read as subcommand arguments") - fmt.Println("\033[1m---- Command List ----\033[0m") - fmt.Println("-> bpm version | shows information on the installed version of bpm") - fmt.Println("-> bpm info [-R] | shows information on an installed package") - fmt.Println(" -R= lets you define the root path which will be used") - fmt.Println("-> bpm list [-R, -c, -n] | lists all installed packages") - fmt.Println(" -R= lets you define the root path which will be used") - fmt.Println(" -c lists the amount of installed packages") - fmt.Println(" -n lists only the names of installed packages") - fmt.Println("-> bpm search | Searches for packages through declared repositories") - fmt.Println("-> bpm install [-R, -v, -y, -f, -o, -c, -b, -k, --reinstall, --reinstall-all, --no-optional] | installs the following files") - fmt.Println(" -R= lets you define the root path which will be used") - fmt.Println(" -v Show additional information about what BPM is doing") - fmt.Println(" -y skips the confirmation prompt") - fmt.Println(" -f skips dependency, conflict and architecture checking") - fmt.Println(" -o= set the binary package output directory (defaults to /var/lib/bpm/compiled)") - fmt.Println(" -c= set the compilation directory (defaults to /var/tmp)") - fmt.Println(" -b creates a binary package from a source package after compilation and saves it in the binary package output directory") - fmt.Println(" -k keeps the compilation directory created by BPM after source package installation") - fmt.Println(" --reinstall Reinstalls packages even if they do not have a newer version available") - fmt.Println(" --reinstall-all Same as --reinstall but also reinstalls dependencies") - fmt.Println(" --no-optional Prevents installation of optional dependencies") - fmt.Println("-> bpm update [-R, -v, -y, -f, --reinstall, --no-sync] | updates all packages that are available in the repositories") - fmt.Println(" -R= lets you define the root path which will be used") - fmt.Println(" -v Show additional information about what BPM is doing") - fmt.Println(" -y skips the confirmation prompt") - fmt.Println(" -f skips dependency, conflict and architecture checking") - fmt.Println(" --reinstall Fetches and reinstalls all packages even if they do not have a newer version available") - fmt.Println(" --no-sync Skips package database syncing") - fmt.Println("-> bpm sync [-R, -v, -y] | Syncs package databases without updating packages") - fmt.Println(" -R= lets you define the root path which will be used") - fmt.Println(" -v Show additional information about what BPM is doing") - fmt.Println(" -y skips the confirmation prompt") - fmt.Println("-> bpm remove [-R, -v, -y] | removes the following packages") - fmt.Println(" -v Show additional information about what BPM is doing") - fmt.Println(" -R= lets you define the root path which will be used") - fmt.Println(" -y skips the confirmation prompt") - fmt.Println("-> bpm file [-R] | shows what packages the following packages are managed by") - fmt.Println(" -R= lets you define the root path which will be used") - fmt.Println("\033[1m----------------\033[0m") -} - -func resolveFlags() { - // List flags - listFlagSet := flag.NewFlagSet("List flags", flag.ExitOnError) - listFlagSet.Usage = printHelp - listFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root") - listFlagSet.BoolVar(&pkgListNumbers, "c", false, "List the number of all packages installed with BPM") - listFlagSet.BoolVar(&pkgListNames, "n", false, "List the names of all packages installed with BPM") - // Info flags - infoFlagSet := flag.NewFlagSet("Info flags", flag.ExitOnError) - infoFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root") - infoFlagSet.Usage = printHelp - // Install flags - installFlagSet := flag.NewFlagSet("Install flags", flag.ExitOnError) - installFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root") - installFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing") - installFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts") - installFlagSet.StringVar(&utils.BPMConfig.BinaryOutputDir, "o", utils.BPMConfig.BinaryOutputDir, "Set the binary output directory") - installFlagSet.StringVar(&utils.BPMConfig.CompilationDir, "c", utils.BPMConfig.CompilationDir, "Set the compilation directory") - installFlagSet.BoolVar(&buildSource, "b", false, "Build binary package from source package") - installFlagSet.BoolVar(&skipCheck, "s", false, "Skip check function during source compilation") - installFlagSet.BoolVar(&keepTempDir, "k", false, "Keep temporary directory after source compilation") - installFlagSet.BoolVar(&force, "f", false, "Force installation by skipping architecture and dependency resolution") - installFlagSet.BoolVar(&reinstall, "reinstall", false, "Reinstalls packages even if they do not have a newer version available") - installFlagSet.BoolVar(&reinstallAll, "reinstall-all", false, "Same as --reinstall but also reinstalls dependencies") - installFlagSet.BoolVar(&noOptional, "no-optional", false, "Prevents installation of optional dependencies") - installFlagSet.Usage = printHelp - // Update flags - updateFlagSet := flag.NewFlagSet("Update flags", flag.ExitOnError) - updateFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root") - updateFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing") - updateFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts") - updateFlagSet.BoolVar(&force, "f", false, "Force update by skipping architecture and dependency resolution") - updateFlagSet.BoolVar(&reinstall, "reinstall", false, "Fetches and reinstalls all packages even if they do not have a newer version available") - updateFlagSet.BoolVar(&nosync, "no-sync", false, "Skips package database syncing") - updateFlagSet.Usage = printHelp - // Sync flags - syncFlagSet := flag.NewFlagSet("Sync flags", flag.ExitOnError) - syncFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root") - syncFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing") - syncFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts") - syncFlagSet.Usage = printHelp - // Remove flags - removeFlagSet := flag.NewFlagSet("Remove flags", flag.ExitOnError) - removeFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root") - removeFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing") - removeFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts") - removeFlagSet.Usage = printHelp - // File flags - fileFlagSet := flag.NewFlagSet("Remove flags", flag.ExitOnError) - fileFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root") - fileFlagSet.Usage = printHelp - if len(os.Args[1:]) <= 0 { - subcommand = "help" - } else { - subcommand = os.Args[1] - subcommandArgs = os.Args[2:] - if getCommandType() == list { - err := listFlagSet.Parse(subcommandArgs) - if err != nil { - return - } - subcommandArgs = listFlagSet.Args() - } else if getCommandType() == info { - err := infoFlagSet.Parse(subcommandArgs) - if err != nil { - return - } - subcommandArgs = infoFlagSet.Args() - } else if getCommandType() == install { - err := installFlagSet.Parse(subcommandArgs) - if err != nil { - return - } - subcommandArgs = installFlagSet.Args() - } else if getCommandType() == update { - err := updateFlagSet.Parse(subcommandArgs) - if err != nil { - return - } - subcommandArgs = updateFlagSet.Args() - } else if getCommandType() == sync { - err := syncFlagSet.Parse(subcommandArgs) - if err != nil { - return - } - subcommandArgs = syncFlagSet.Args() - } else if getCommandType() == remove { - err := removeFlagSet.Parse(subcommandArgs) - if err != nil { - return - } - subcommandArgs = removeFlagSet.Args() - } else if getCommandType() == file { - err := fileFlagSet.Parse(subcommandArgs) - if err != nil { - return - } - subcommandArgs = fileFlagSet.Args() - } - if reinstallAll { - reinstall = true - } - } -} diff --git a/src/bpm/go.mod b/src/bpm/go.mod new file mode 100644 index 0000000..5129f8b --- /dev/null +++ b/src/bpm/go.mod @@ -0,0 +1,14 @@ +module git.enumerated.dev/bubble-package-manager/bpm/src/bpm + +go 1.23 + +toolchain go1.23.7 + +require git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib v0.5.0 + +replace git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib => ../bpmlib + +require ( + github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/src/bpm/go.sum similarity index 57% rename from go.sum rename to src/bpm/go.sum index 138d9eb..8a51310 100644 --- a/go.sum +++ b/src/bpm/go.sum @@ -1,9 +1,6 @@ -github.com/elliotchance/orderedmap/v2 v2.4.0 h1:6tUmMwD9F998FNpwFxA5E6NQvSpk2PVw7RKsVq3+2Cw= -github.com/elliotchance/orderedmap/v2 v2.4.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q= -github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= -github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f h1:xt29M2T6STgldg+WEP51gGePQCsQvklmP2eIhPIBK3g= github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/bpm/main.go b/src/bpm/main.go new file mode 100644 index 0000000..41b3440 --- /dev/null +++ b/src/bpm/main.go @@ -0,0 +1,950 @@ +package main + +import ( + "bufio" + "errors" + "flag" + "fmt" + "log" + "os" + "os/exec" + "path" + "path/filepath" + "slices" + "strings" + + "git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib" +) + +/* -------------BPM | Bubble Package Manager-------------- */ +/* Made By EnumDev (Previously CapCreeperGR) */ +/* A simple-to-use package manager */ +/* ------------------------------------------------------- */ + +var bpmVer = "0.5.0" + +var subcommand = "help" +var subcommandArgs []string + +// Flags +var rootDir = "/" +var verbose = false +var yesAll = false +var force = false +var pkgListNumbers = false +var pkgListNames = false +var reinstall = false +var reinstallAll = false +var installOptional = false +var installationReason = "" +var nosync = true +var removeUnused = false +var doCleanup = false +var showDatabaseInfo = false +var installSrcPkgDepends = false +var skipChecks = false +var outputDirectory = "" +var cleanupDependencies = false +var cleanupMakeDependencies = false +var cleanupCompilationFiles = false +var cleanupCompiledPackages = false +var cleanupFetchedPackages = false + +func main() { + err := bpmlib.ReadConfig() + if err != nil { + log.Fatalf("Error: could not read BPM config: %s", err) + } + resolveFlags() + resolveCommand() +} + +type commandType uint8 + +const ( + _default commandType = iota + help + info + list + search + install + update + sync + remove + cleanup + file + compile +) + +func getCommandType() commandType { + switch subcommand { + case "version": + return _default + case "info": + return info + case "list": + return list + case "search": + return search + case "install": + return install + case "update": + return update + case "sync": + return sync + case "remove": + return remove + case "cleanup": + return cleanup + case "file": + return file + case "compile": + return compile + default: + return help + } +} + +func resolveCommand() { + switch getCommandType() { + case _default: + fmt.Println("Bubble Package Manager (BPM)") + fmt.Println("Version: " + bpmVer) + case info: + packages := subcommandArgs + if len(packages) == 0 { + fmt.Println("No packages were given") + return + } + + // Read local databases + err := bpmlib.ReadLocalDatabaseFiles() + if err != nil { + log.Fatalf("Error: could not read local databases: %s", err) + } + + for n, pkg := range packages { + var info *bpmlib.PackageInfo + isFile := false + showInstallationReason := false + if showDatabaseInfo { + var err error + var entry *bpmlib.BPMDatabaseEntry + entry, _, err = bpmlib.GetDatabaseEntry(pkg) + if err != nil { + if entry = bpmlib.ResolveVirtualPackage(pkg); entry == nil { + log.Fatalf("Error: could not find package (%s) in any database\n", pkg) + } + } + info = entry.Info + } else if stat, err := os.Stat(pkg); err == nil && !stat.IsDir() { + bpmpkg, err := bpmlib.ReadPackage(pkg) + if err != nil { + log.Fatalf("Error: could not read package: %s\n", err) + } + info = bpmpkg.PkgInfo + isFile = true + } else { + if isVirtual, p := bpmlib.IsVirtualPackage(pkg, rootDir); isVirtual { + info = bpmlib.GetPackageInfo(p, rootDir) + } else { + info = bpmlib.GetPackageInfo(pkg, rootDir) + } + showInstallationReason = true + } + if info == nil { + log.Fatalf("Error: package (%s) is not installed\n", pkg) + } + if n != 0 { + fmt.Println() + } + if isFile { + abs, err := filepath.Abs(pkg) + if err != nil { + log.Fatalf("Error: could not get absolute path of file (%s)\n", abs) + } + fmt.Println("File: " + abs) + } + fmt.Println(bpmlib.CreateReadableInfo(true, true, true, showInstallationReason, info, rootDir)) + } + case list: + // Read local databases + err := bpmlib.ReadLocalDatabaseFiles() + if err != nil { + log.Fatalf("Error: could not read local databases: %s", err) + } + + packages, err := bpmlib.GetInstalledPackages(rootDir) + if err != nil { + log.Fatalf("Error: could not get installed packages: %s", err.Error()) + return + } + if pkgListNumbers { + fmt.Println(len(packages)) + } else if pkgListNames { + for _, pkg := range packages { + fmt.Println(pkg) + } + } else { + if len(packages) == 0 { + fmt.Println("No packages have been installed") + return + } + for n, pkg := range packages { + info := bpmlib.GetPackageInfo(pkg, rootDir) + if info == nil { + fmt.Printf("Package (%s) could not be found\n", pkg) + continue + } + if n != 0 { + fmt.Println() + } + fmt.Println(bpmlib.CreateReadableInfo(true, true, true, true, info, rootDir)) + } + } + case search: + searchTerms := subcommandArgs + if len(searchTerms) == 0 { + log.Fatalf("Error: no search terms given") + } + + // Read local databases + err := bpmlib.ReadLocalDatabaseFiles() + if err != nil { + log.Fatalf("Error: could not read local databases: %s", err) + } + + for i, term := range searchTerms { + nameResults := make([]*bpmlib.PackageInfo, 0) + descResults := make([]*bpmlib.PackageInfo, 0) + for _, db := range bpmlib.MainBPMConfig.Databases { + for _, entry := range db.Entries { + if strings.Contains(entry.Info.Name, term) { + nameResults = append(nameResults, entry.Info) + } else if strings.Contains(entry.Info.Description, term) { + descResults = append(descResults, entry.Info) + } + } + } + results := append(nameResults, descResults...) + if len(results) == 0 { + log.Fatalf("Error: no results for term (%s) were found\n", term) + } + if i > 0 { + fmt.Println() + } + fmt.Printf("Results for term (%s)\n", term) + for j, result := range results { + fmt.Printf("%d) %s: %s (%s)\n", j+1, result.Name, result.Description, result.GetFullVersion()) + } + } + case install: + // Check for required permissions + if os.Getuid() != 0 { + log.Fatalf("Error: this subcommand needs to be run with superuser permissions") + } + + // Return if no packages are specified + if len(subcommandArgs) == 0 { + fmt.Println("No packages or files were given to install") + return + } + + // Check if installationReason argument is valid + ir := bpmlib.InstallationReasonUnknown + switch installationReason { + case "manual": + ir = bpmlib.InstallationReasonManual + case "dependency": + ir = bpmlib.InstallationReasonDependency + case "make-dependency": + ir = bpmlib.InstallationReasonMakeDependency + case "": + default: + log.Fatalf("Error: %s is not a valid installation reason", installationReason) + } + + // Get reinstall method + var reinstallMethod bpmlib.ReinstallMethod + if reinstallAll { + reinstallMethod = bpmlib.ReinstallMethodAll + } else if reinstall { + reinstallMethod = bpmlib.ReinstallMethodSpecified + } else { + reinstallMethod = bpmlib.ReinstallMethodNone + } + + // Read local databases + err := bpmlib.ReadLocalDatabaseFiles() + if err != nil { + log.Fatalf("Error: could not read local databases: %s", err) + } + + // Create installation operation + operation, err := bpmlib.InstallPackages(rootDir, ir, reinstallMethod, installOptional, force, verbose, subcommandArgs...) + if errors.As(err, &bpmlib.PackageNotFoundErr{}) || errors.As(err, &bpmlib.DependencyNotFoundErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) { + log.Fatalf("Error: %s", err) + } else if err != nil { + log.Fatalf("Error: could not setup operation: %s\n", err) + } + + // Exit if operation contains no actions + if len(operation.Actions) == 0 { + fmt.Println("No action needs to be taken") + return + } + + // Show operation summary + operation.ShowOperationSummary() + + // Confirmation Prompt + if !yesAll { + reader := bufio.NewReader(os.Stdin) + if len(operation.Actions) == 1 { + fmt.Printf("Do you wish to install this package? [y\\N] ") + } else { + fmt.Printf("Do you wish to install these %d packages? [y\\N] ", len(operation.Actions)) + } + + text, _ := reader.ReadString('\n') + if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" { + fmt.Println("Cancelling package installation...") + os.Exit(1) + } + } + + // Execute operation + err = operation.Execute(verbose, force) + if err != nil { + log.Fatalf("Error: could not complete operation: %s\n", err) + } + + // Executing hooks + fmt.Println("Running hooks...") + err = operation.RunHooks(verbose) + if err != nil { + log.Fatalf("Error: could not run hooks: %s\n", err) + } + case update: + // Check for required permissions + if os.Getuid() != 0 { + log.Fatalf("Error: this subcommand needs to be run with superuser permissions") + } + + // Read local databases if no sync + if nosync { + err := bpmlib.ReadLocalDatabaseFiles() + if err != nil { + log.Fatalf("Error: could not read local databases: %s", err) + } + } + + // Create update operation + operation, err := bpmlib.UpdatePackages(rootDir, !nosync, installOptional, force, verbose) + if errors.As(err, &bpmlib.PackageNotFoundErr{}) || errors.As(err, &bpmlib.DependencyNotFoundErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) { + log.Fatalf("Error: %s", err) + } else if err != nil { + log.Fatalf("Error: could not setup operation: %s\n", err) + } + + // Exit if operation contains no actions + if len(operation.Actions) == 0 { + fmt.Println("No action needs to be taken") + return + } + + // Show operation summary + operation.ShowOperationSummary() + + // Confirmation Prompt + if !yesAll { + fmt.Printf("Are you sure you wish to update all %d packages? [y\\N] ", len(operation.Actions)) + reader := bufio.NewReader(os.Stdin) + text, _ := reader.ReadString('\n') + if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" { + fmt.Println("Cancelling package update...") + os.Exit(1) + } + } + + // Execute operation + err = operation.Execute(verbose, force) + if err != nil { + log.Fatalf("Error: could not complete operation: %s\n", err) + } + + // Executing hooks + fmt.Println("Running hooks...") + err = operation.RunHooks(verbose) + if err != nil { + log.Fatalf("Error: could not run hooks: %s\n", err) + } + case sync: + // Check for required permissions + if os.Getuid() != 0 { + log.Fatalf("Error: this subcommand needs to be run with superuser permissions") + } + + // Confirmation Prompt + if !yesAll { + fmt.Printf("Are you sure you wish to sync all databases? [y\\N] ") + reader := bufio.NewReader(os.Stdin) + text, _ := reader.ReadString('\n') + if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" { + fmt.Println("Cancelling database synchronization...") + os.Exit(1) + } + } + + // Sync databases + err := bpmlib.SyncDatabase(verbose) + if err != nil { + log.Fatalf("Error: could not sync local database: %s\n", err) + } + + fmt.Println("All package databases synced successfully!") + case remove: + // Check for required permissions + if os.Getuid() != 0 { + log.Fatalf("Error: this subcommand needs to be run with superuser permissions") + } + + if len(subcommandArgs) == 0 { + fmt.Println("No packages were given") + return + } + + // Read local databases + err := bpmlib.ReadLocalDatabaseFiles() + if err != nil { + log.Fatalf("Error: could not read local databases: %s", err) + } + + // Create remove operation + operation, err := bpmlib.RemovePackages(rootDir, removeUnused, doCleanup, subcommandArgs...) + if errors.As(err, &bpmlib.PackageNotFoundErr{}) || errors.As(err, &bpmlib.DependencyNotFoundErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) { + log.Fatalf("Error: %s", err) + } else if err != nil { + log.Fatalf("Error: could not setup operation: %s\n", err) + } + + // Exit if operation contains no actions + if len(operation.Actions) == 0 { + fmt.Println("No action needs to be taken") + return + } + + // Show operation summary + operation.ShowOperationSummary() + + // Confirmation Prompt + if !yesAll { + fmt.Printf("Are you sure you wish to remove all %d packages? [y\\N] ", len(operation.Actions)) + reader := bufio.NewReader(os.Stdin) + text, _ := reader.ReadString('\n') + if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" { + fmt.Println("Cancelling package removal...") + os.Exit(1) + } + } + + // Execute operation + err = operation.Execute(verbose, force) + if err != nil { + log.Fatalf("Error: could not complete operation: %s\n", err) + } + + // Executing hooks + fmt.Println("Running hooks...") + err = operation.RunHooks(verbose) + if err != nil { + log.Fatalf("Error: could not run hooks: %s\n", err) + } + case cleanup: + // Check for required permissions + if os.Getuid() != 0 { + log.Fatalf("Error: this subcommand needs to be run with superuser permissions") + } + + err := bpmlib.CleanupCache(rootDir, cleanupCompilationFiles, cleanupCompiledPackages, cleanupFetchedPackages, verbose) + if err != nil { + log.Fatalf("Error: could not complete cache cleanup: %s", err) + } + + if cleanupDependencies || cleanupMakeDependencies { + // Read local databases + err := bpmlib.ReadLocalDatabaseFiles() + if err != nil { + log.Fatalf("Error: could not read local databases: %s", err) + } + + // Create cleanup operation + operation, err := bpmlib.CleanupPackages(cleanupMakeDependencies, rootDir) + if errors.As(err, &bpmlib.PackageNotFoundErr{}) || errors.As(err, &bpmlib.DependencyNotFoundErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) { + log.Fatalf("Error: %s", err) + } else if err != nil { + log.Fatalf("Error: could not setup operation: %s\n", err) + } + + // Exit if operation contains no actions + if len(operation.Actions) == 0 { + fmt.Println("No action needs to be taken") + return + } + + // Show operation summary + operation.ShowOperationSummary() + + // Confirmation Prompt + if !yesAll { + fmt.Printf("Are you sure you wish to remove all %d packages? [y\\N] ", len(operation.Actions)) + reader := bufio.NewReader(os.Stdin) + text, _ := reader.ReadString('\n') + if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" { + fmt.Println("Cancelling package removal...") + os.Exit(1) + } + } + + // Execute operation + err = operation.Execute(verbose, force) + if err != nil { + log.Fatalf("Error: could not complete operation: %s\n", err) + } + + // Executing hooks + fmt.Println("Running hooks...") + err = operation.RunHooks(verbose) + if err != nil { + log.Fatalf("Error: could not run hooks: %s\n", err) + } + } + case file: + files := subcommandArgs + if len(files) == 0 { + fmt.Println("No files were given to get which packages manage it") + return + } + for _, file := range files { + absFile, err := filepath.Abs(file) + if err != nil { + log.Fatalf("Error: could not get absolute path of file (%s)\n", file) + } + stat, err := os.Stat(absFile) + if os.IsNotExist(err) { + log.Fatalf("Error: file (%s) does not exist!\n", absFile) + } + pkgs, err := bpmlib.GetInstalledPackages(rootDir) + if err != nil { + log.Fatalf("Error: could not get installed packages: %s\n", err.Error()) + } + + if !strings.HasPrefix(absFile, rootDir) { + log.Fatalf("Error: could not get path of file (%s) relative to root path", absFile) + } + absFile, err = filepath.Rel(rootDir, absFile) + if err != nil { + log.Fatalf("Error: could not get path of file (%s) relative to root path", absFile) + } + absFile = strings.TrimPrefix(absFile, "/") + if stat.IsDir() { + absFile = absFile + "/" + } + + var pkgList []string + for _, pkg := range pkgs { + if slices.ContainsFunc(bpmlib.GetPackage(pkg, rootDir).PkgFiles, func(entry *bpmlib.PackageFileEntry) bool { + return entry.Path == absFile + }) { + pkgList = append(pkgList, pkg) + } + } + if len(pkgList) == 0 { + fmt.Println(absFile + " is not managed by any packages") + } else { + fmt.Println(absFile + " is managed by the following packages:") + for _, pkg := range pkgList { + fmt.Println("- " + pkg) + } + } + } + case compile: + if len(subcommandArgs) == 0 { + fmt.Println("No source packages were given") + return + } + + // Read local databases + err := bpmlib.ReadLocalDatabaseFiles() + if err != nil { + log.Fatalf("Error: could not read local databases: %s", err) + } + + // Compile packages + for _, sourcePackage := range subcommandArgs { + if _, err := os.Stat(sourcePackage); os.IsNotExist(err) { + log.Fatalf("Error: file (%s) does not exist!", sourcePackage) + } + + // Read archive + bpmpkg, err := bpmlib.ReadPackage(sourcePackage) + if err != nil { + log.Fatalf("Could not read package (%s): %s", sourcePackage, err) + } + + // Ensure archive is source BPM package + if bpmpkg.PkgInfo.Type != "source" { + log.Fatalf("Error: cannot compile a non-source package!") + } + + // Get direct runtime and make dependencies + totalDepends := make([]string, 0) + for _, depend := range bpmpkg.PkgInfo.GetDependencies(true, false) { + if !slices.Contains(totalDepends, depend.PkgName) { + totalDepends = append(totalDepends, depend.PkgName) + } + } + + // Get unmet dependencies + unmetDepends := slices.Clone(totalDepends) + installedPackages, err := bpmlib.GetInstalledPackages("/") + if err != nil { + log.Fatalf("Error: could not get installed packages: %s\n", err) + } + for i := len(unmetDepends) - 1; i >= 0; i-- { + if slices.Contains(installedPackages, unmetDepends[i]) { + unmetDepends = append(unmetDepends[:i], unmetDepends[i+1:]...) + } else if ok, _ := bpmlib.IsVirtualPackage(unmetDepends[i], rootDir); ok { + unmetDepends = append(unmetDepends[:i], unmetDepends[i+1:]...) + } + } + + // Install missing source package dependencies + if installSrcPkgDepends && len(unmetDepends) > 0 { + // Get path to current executable + executable, err := os.Executable() + if err != nil { + log.Fatalf("Error: could not get path to executable: %s\n", err) + } + + // Run 'bpm install' using the set privilege escalator command + args := []string{executable, "install", "--installation-reason=make-dependency"} + args = append(args, unmetDepends...) + cmd := exec.Command(bpmlib.CompilationBPMConfig.PrivilegeEscalatorCmd, args...) + if yesAll { + cmd.Args = slices.Insert(cmd.Args, 3, "-y") + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + if verbose { + fmt.Println("Running command: " + cmd.String()) + } + err = cmd.Run() + if err != nil { + log.Fatalf("Error: dependency installation command failed: %s\n", err) + } + } else { + // Ensure the required dependencies are installed + if len(unmetDepends) != 0 { + log.Fatalf("Error: could not resolve dependencies: the following dependencies were not found in any databases: " + strings.Join(unmetDepends, ", ")) + } + } + + // Get current working directory + workdir, err := os.Getwd() + if err != nil { + log.Fatalf("Error: could not get working directory: %s", err) + } + + // Get user home directory + homedir, err := os.UserHomeDir() + if err != nil { + log.Fatalf("Error: could not get user home directory: %s", err) + } + + // Trim output directory + outputDirectory = strings.TrimSpace(outputDirectory) + if outputDirectory != "/" { + outputDirectory = strings.TrimSuffix(outputDirectory, "/") + } + + // Set output directory if empty + if outputDirectory == "" { + outputDirectory = workdir + } + + // Replace first tilde with user home directory + if strings.Split(outputDirectory, "/")[0] == "~" { + outputDirectory = strings.Replace(outputDirectory, "~", homedir, 1) + } + + // Prepend current working directory to output directory if not an absolute path + if outputDirectory != "" && !strings.HasPrefix(outputDirectory, "/") { + outputDirectory = filepath.Join(workdir, outputDirectory) + } + + // Clean path + path.Clean(outputDirectory) + + // Ensure output directory exists and is a directory + stat, err := os.Stat(outputDirectory) + if err != nil { + log.Fatalf("Error: could not stat output directory (%s): %s", outputDirectory, err) + } + if !stat.IsDir() { + log.Fatalf("Error: output directory (%s) is not a directory", outputDirectory) + } + + outputBpmPackages, err := bpmlib.CompileSourcePackage(sourcePackage, outputDirectory, skipChecks) + if err != nil { + log.Fatalf("Error: could not compile source package (%s): %s", sourcePackage, err) + } + + for k, v := range outputBpmPackages { + fmt.Printf("Package (%s) was successfully compiled! Binary package generated at: %s\n", k, v) + } + + // Remove unused packages + if installSrcPkgDepends && len(unmetDepends) > 0 { + // Get path to current executable + executable, err := os.Executable() + if err != nil { + log.Fatalf("Error: could not get path to executable: %s\n", err) + } + + // Run 'bpm cleanup' using the set privilege escalator command + cmd := exec.Command(bpmlib.CompilationBPMConfig.PrivilegeEscalatorCmd, executable, "cleanup") + if yesAll { + cmd.Args = slices.Insert(cmd.Args, 3, "-y") + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + if verbose { + fmt.Println("Running command: " + cmd.String()) + } + err = cmd.Run() + if err != nil { + log.Fatalf("Error: dependency cleanup command failed: %s\n", err) + } + } + } + + default: + printHelp() + } +} + +func printHelp() { + fmt.Println("\033[1m---- Command Format ----\033[0m") + fmt.Println("-> command format: bpm [-flags]...") + fmt.Println("-> flags will be read if passed right after the subcommand otherwise they will be read as subcommand arguments") + fmt.Println("\033[1m---- Command List ----\033[0m") + fmt.Println("-> bpm version | shows information on the installed version of bpm") + fmt.Println("-> bpm info [-R, --databases] | shows information on an installed package") + fmt.Println(" -R= lets you define the root path which will be used") + fmt.Println(" --databases show information on package in configured databases") + fmt.Println("-> bpm list [-R, -c, -n] | lists all installed packages") + fmt.Println(" -R= lets you define the root path which will be used") + fmt.Println(" -c lists the amount of installed packages") + fmt.Println(" -n lists only the names of installed packages") + fmt.Println("-> bpm search | Searches for packages through configured databases") + fmt.Println("-> bpm install [-R, -v, -y, -f, --reinstall, --reinstall-all, --no-optional, --installation-reason] | installs the following files") + fmt.Println(" -R= lets you define the root path which will be used") + fmt.Println(" -v Show additional information about what BPM is doing") + fmt.Println(" -y skips the confirmation prompt") + fmt.Println(" -f skips dependency, conflict and architecture checking") + fmt.Println(" --reinstall Reinstalls packages even if they do not have a newer version available") + fmt.Println(" --reinstall-all Same as --reinstall but also reinstalls dependencies") + fmt.Println(" --optional Installs all optional dependencies") + fmt.Println(" --installation-reason= sets the installation reason for all newly installed packages") + fmt.Println("-> bpm update [-R, -v, -y, -f, --reinstall, --no-sync] | updates all packages that are available in the configured databases") + fmt.Println(" -R= lets you define the root path which will be used") + fmt.Println(" -v Show additional information about what BPM is doing") + fmt.Println(" -y skips the confirmation prompt") + fmt.Println(" -f skips dependency, conflict and architecture checking") + fmt.Println(" --reinstall Fetches and reinstalls all packages even if they do not have a newer version available") + fmt.Println(" --no-sync Skips package database syncing") + fmt.Println("-> bpm sync [-R, -v, -y] | Syncs package databases without updating packages") + fmt.Println(" -R= lets you define the root path which will be used") + fmt.Println(" -v Show additional information about what BPM is doing") + fmt.Println(" -y skips the confirmation prompt") + fmt.Println("-> bpm remove [-R, -v, -y, --unused, --cleanup] | removes the following packages") + fmt.Println(" -v Show additional information about what BPM is doing") + fmt.Println(" -R= lets you define the root path which will be used") + fmt.Println(" -y skips the confirmation prompt") + fmt.Println(" --unused removes only packages that aren't required as dependencies by other packages") + fmt.Println(" --cleanup performs a dependency cleanup") + fmt.Println("-> bpm cleanup [-R, -v, -y, --depends, --compilation-files, --compiled-pkgs, --fetched-pkgs] | remove all unused dependencies and cache directories") + fmt.Println(" -v Show additional information about what BPM is doing") + fmt.Println(" -R= lets you define the root path which will be used") + fmt.Println(" -y skips the confirmation prompt") + fmt.Println(" --depends performs a dependency cleanup") + fmt.Println(" --make-depends performs a make dependency cleanup") + fmt.Println(" --compilation-files performs a cleanup of compilation files") + fmt.Println(" --compiled-pkgs performs a cleanup of compilation compiled binary packages") + fmt.Println(" --fetched-pkgs performs a cleanup of fetched packages from databases") + fmt.Println("-> bpm file [-R] | shows what packages the following packages are managed by") + fmt.Println(" -R= lets you define the root path which will be used") + fmt.Println("-> bpm compile [-d, -s, -o] | Compile source BPM package") + fmt.Println(" -v Show additional information about what BPM is doing") + fmt.Println(" -d installs required dependencies for package compilation") + fmt.Println(" -s skips the check function in source.sh scripts") + fmt.Println(" -o sets output directory") + fmt.Println(" -y skips the confirmation prompt") + + fmt.Println("\033[1m----------------\033[0m") +} + +func resolveFlags() { + // List flags + listFlagSet := flag.NewFlagSet("List flags", flag.ExitOnError) + listFlagSet.Usage = printHelp + listFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root") + listFlagSet.BoolVar(&pkgListNumbers, "c", false, "List the number of all packages installed with BPM") + listFlagSet.BoolVar(&pkgListNames, "n", false, "List the names of all packages installed with BPM") + // Info flags + infoFlagSet := flag.NewFlagSet("Info flags", flag.ExitOnError) + infoFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root") + infoFlagSet.BoolVar(&showDatabaseInfo, "databases", false, "Show information on package in configured databases") + infoFlagSet.Usage = printHelp + // Install flags + installFlagSet := flag.NewFlagSet("Install flags", flag.ExitOnError) + installFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root") + installFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing") + installFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts") + installFlagSet.BoolVar(&force, "f", false, "Force installation by skipping architecture and dependency resolution") + installFlagSet.BoolVar(&reinstall, "reinstall", false, "Reinstalls packages even if they do not have a newer version available") + installFlagSet.BoolVar(&reinstallAll, "reinstall-all", false, "Same as --reinstall but also reinstalls dependencies") + installFlagSet.BoolVar(&installOptional, "optional", false, "Installs all optional dependencies") + installFlagSet.StringVar(&installationReason, "installation-reason", "", "Set the installation reason for all newly installed packages") + installFlagSet.Usage = printHelp + // Update flags + updateFlagSet := flag.NewFlagSet("Update flags", flag.ExitOnError) + updateFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root") + updateFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing") + updateFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts") + updateFlagSet.BoolVar(&force, "f", false, "Force update by skipping architecture and dependency resolution") + updateFlagSet.BoolVar(&nosync, "no-sync", false, "Skips package database syncing") + updateFlagSet.Usage = printHelp + // Sync flags + syncFlagSet := flag.NewFlagSet("Sync flags", flag.ExitOnError) + syncFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root") + syncFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing") + syncFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts") + syncFlagSet.Usage = printHelp + // Remove flags + removeFlagSet := flag.NewFlagSet("Remove flags", flag.ExitOnError) + removeFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root") + removeFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing") + removeFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts") + removeFlagSet.BoolVar(&removeUnused, "unused", false, "Removes only packages that aren't required as dependencies by other packages") + removeFlagSet.BoolVar(&doCleanup, "cleanup", false, "Perform a dependency cleanup") + removeFlagSet.Usage = printHelp + // Cleanup flags + cleanupFlagSet := flag.NewFlagSet("Cleanup flags", flag.ExitOnError) + cleanupFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root") + cleanupFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing") + cleanupFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts") + cleanupFlagSet.BoolVar(&cleanupDependencies, "depends", false, "Perform a dependency cleanup") + cleanupFlagSet.BoolVar(&cleanupMakeDependencies, "make-depends", false, "Perform a make dependency cleanup") + cleanupFlagSet.BoolVar(&cleanupCompilationFiles, "compilation-files", false, "Perform a cleanup of compilation files") + cleanupFlagSet.BoolVar(&cleanupCompiledPackages, "compiled-pkgs", false, "Perform a cleanup of compilation compiled binary packages") + cleanupFlagSet.BoolVar(&cleanupFetchedPackages, "fetched-pkgs", false, "Perform a cleanup of fetched packages from databases") + cleanupFlagSet.Usage = printHelp + // File flags + fileFlagSet := flag.NewFlagSet("Remove flags", flag.ExitOnError) + fileFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root") + fileFlagSet.Usage = printHelp + // Compile flags + compileFlagSet := flag.NewFlagSet("Compile flags", flag.ExitOnError) + compileFlagSet.BoolVar(&installSrcPkgDepends, "d", false, "Install required dependencies for package compilation") + compileFlagSet.BoolVar(&skipChecks, "s", false, "Skip the check function in source.sh scripts") + compileFlagSet.StringVar(&outputDirectory, "o", "", "Set output directory") + compileFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing") + compileFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts") + compileFlagSet.Usage = printHelp + + isFlagSet := func(flagSet *flag.FlagSet, name string) bool { + found := false + flagSet.Visit(func(f *flag.Flag) { + if f.Name == name { + found = true + } + }) + return found + } + + if len(os.Args[1:]) <= 0 { + subcommand = "help" + } else { + subcommand = os.Args[1] + subcommandArgs = os.Args[2:] + if getCommandType() == list { + err := listFlagSet.Parse(subcommandArgs) + if err != nil { + return + } + subcommandArgs = listFlagSet.Args() + } else if getCommandType() == info { + err := infoFlagSet.Parse(subcommandArgs) + if err != nil { + return + } + subcommandArgs = infoFlagSet.Args() + } else if getCommandType() == install { + err := installFlagSet.Parse(subcommandArgs) + if err != nil { + return + } + subcommandArgs = installFlagSet.Args() + } else if getCommandType() == update { + err := updateFlagSet.Parse(subcommandArgs) + if err != nil { + return + } + subcommandArgs = updateFlagSet.Args() + } else if getCommandType() == sync { + err := syncFlagSet.Parse(subcommandArgs) + if err != nil { + return + } + subcommandArgs = syncFlagSet.Args() + } else if getCommandType() == remove { + err := removeFlagSet.Parse(subcommandArgs) + if err != nil { + return + } + subcommandArgs = removeFlagSet.Args() + } else if getCommandType() == cleanup { + err := cleanupFlagSet.Parse(subcommandArgs) + if err != nil { + return + } + if !isFlagSet(cleanupFlagSet, "depends") && !isFlagSet(cleanupFlagSet, "make-depends") && !isFlagSet(cleanupFlagSet, "compilation-files") && !isFlagSet(cleanupFlagSet, "compiled-pkgs") && !isFlagSet(cleanupFlagSet, "fetched-pkgs") { + cleanupDependencies = true + cleanupMakeDependencies = bpmlib.MainBPMConfig.CleanupMakeDependencies + cleanupCompilationFiles = true + cleanupCompiledPackages = true + cleanupFetchedPackages = true + } + subcommandArgs = cleanupFlagSet.Args() + } else if getCommandType() == file { + err := fileFlagSet.Parse(subcommandArgs) + if err != nil { + return + } + subcommandArgs = fileFlagSet.Args() + } else if getCommandType() == compile { + err := compileFlagSet.Parse(subcommandArgs) + if err != nil { + return + } + subcommandArgs = compileFlagSet.Args() + } + if reinstallAll { + reinstall = true + } + } +} diff --git a/src/bpmlib/compilation.go b/src/bpmlib/compilation.go new file mode 100644 index 0000000..1e2d2e1 --- /dev/null +++ b/src/bpmlib/compilation.go @@ -0,0 +1,383 @@ +package bpmlib + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path" + "strconv" + "strings" + "syscall" + + "gopkg.in/yaml.v3" +) + +var rootCompilationUID = "65534" +var rootCompilationGID = "65534" + +func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bool) (outputBpmPackages map[string]string, err error) { + // Initialize map + outputBpmPackages = make(map[string]string) + + // Read BPM archive + bpmpkg, err := ReadPackage(archiveFilename) + if err != nil { + return nil, err + } + + // Ensure package type is 'source' + if bpmpkg.PkgInfo.Type != "source" { + return nil, errors.New("cannot compile a non-source package") + } + + // Read compilation options file in current directory + compilationOptions, err := readCompilationOptionsFile() + if err != nil { + return nil, err + } + + // Get HOME directory + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + + // Get UID and GID to use for compilation + var uid, gid int + if os.Getuid() == 0 { + _uid, err := strconv.ParseInt(rootCompilationUID, 10, 32) + if err != nil { + return nil, fmt.Errorf("could not convert UID '%s' to int", rootCompilationUID) + } + _gid, err := strconv.ParseInt(rootCompilationGID, 10, 32) + if err != nil { + return nil, fmt.Errorf("could not convert GID '%s' to int", rootCompilationGID) + } + uid = int(_uid) + gid = int(_gid) + } else { + uid = os.Getuid() + gid = os.Getgid() + } + + // Set temporary directory + var tempDirectory string + if os.Getuid() == 0 { + tempDirectory = path.Join("/var/cache/bpm/compilation/", bpmpkg.PkgInfo.Name) + } else { + tempDirectory = path.Join(homeDir, ".cache/bpm/compilation/", bpmpkg.PkgInfo.Name) + } + + // Ensure temporary directory does not exist + if _, err := os.Stat(tempDirectory); err == nil { + err := os.RemoveAll(tempDirectory) + if err != nil { + return nil, err + } + } + + // Create temporary directory + err = os.MkdirAll(tempDirectory, 0755) + if err != nil { + return nil, err + } + + // Change temporary directory owner + err = os.Chown(tempDirectory, uid, gid) + if err != nil { + return nil, err + } + + // Extract source.sh file + err = extractTarballFile(archiveFilename, "source.sh", tempDirectory, uid, gid) + if err != nil { + return nil, err + } + + // Get package scripts and extract them + packageScripts := getPackageScripts(archiveFilename) + for _, script := range packageScripts { + err = extractTarballFile(archiveFilename, script, tempDirectory, uid, gid) + if err != nil { + return nil, err + } + } + + // Extract source files + err = extractTarballDirectory(archiveFilename, "source-files", tempDirectory, uid, gid) + if err != nil { + return nil, err + } + + // Create source directory + err = os.Mkdir(path.Join(tempDirectory, "source"), 0755) + if err != nil { + return nil, err + } + + // Change source directory owner + err = os.Chown(path.Join(tempDirectory, "source"), uid, gid) + if err != nil { + return nil, err + } + + // Setup environment for commands + env := os.Environ() + env = append(env, "HOME="+tempDirectory) + env = append(env, "BPM_WORKDIR="+tempDirectory) + env = append(env, "BPM_SOURCE="+path.Join(tempDirectory, "source")) + env = append(env, "BPM_OUTPUT="+path.Join(tempDirectory, "output")) + env = append(env, "BPM_PKG_NAME="+bpmpkg.PkgInfo.Name) + env = append(env, "BPM_PKG_VERSION="+bpmpkg.PkgInfo.Version) + env = append(env, "BPM_PKG_REVISION="+strconv.Itoa(bpmpkg.PkgInfo.Revision)) + // Check for architecture override in compilation options + if val, ok := compilationOptions["ARCH"]; ok { + env = append(env, "BPM_PKG_ARCH="+val) + } else { + env = append(env, "BPM_PKG_ARCH="+GetArch()) + } + env = append(env, CompilationBPMConfig.CompilationEnvironment...) + + // Execute prepare and build functions in source.sh script + cmd := exec.Command("bash", "-c", + "set -a\n"+ // Source and export functions and variables in source.sh script + ". \"${BPM_WORKDIR}\"/source.sh\n"+ + "set +a\n"+ + "[[ $(type -t prepare) == \"function\" ]] && { echo \"Running prepare() function\"; bash -e -c 'cd \"$BPM_WORKDIR\" && prepare' || exit 1; }\n"+ // Run prepare() function if it exists + "[[ $(type -t build) == \"function\" ]] && { echo \"Running build() function\"; bash -e -c 'cd \"$BPM_SOURCE\" && build' || exit 1; }\n"+ // Run build() function if it exists + "exit 0") + cmd.Dir = tempDirectory + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = env + if os.Getuid() == 0 { + cmd.SysProcAttr = &syscall.SysProcAttr{} + cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)} + } + err = cmd.Run() + if err != nil { + return nil, err + } + + // Execute check function in source.sh script if not skipping checks + if !skipChecks { + cmd = exec.Command("bash", "-c", + "set -a\n"+ // Source and export functions and variables in source.sh script + ". \"${BPM_WORKDIR}\"/source.sh\n"+ + "set +a\n"+ + "[[ $(type -t check) == \"function\" ]] && { echo \"Running check() function\"; bash -e -c 'cd \"$BPM_SOURCE\" && check' || exit 1; }\n"+ // Run check() function if it exists + "exit 0") + cmd.Dir = tempDirectory + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = env + if os.Getuid() == 0 { + cmd.SysProcAttr = &syscall.SysProcAttr{} + cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)} + } + err = cmd.Run() + if err != nil { + return nil, err + } + } + + // Get all packages to compile + packagesToCompile := bpmpkg.PkgInfo.SplitPackages + if !bpmpkg.PkgInfo.IsSplitPackage() { + packagesToCompile = append(packagesToCompile, bpmpkg.PkgInfo) + } + + // Compile each package + for _, pkg := range packagesToCompile { + // Get package function name + packageFunctionName := "package" + if bpmpkg.PkgInfo.IsSplitPackage() { + packageFunctionName = "package_" + pkg.Name + } + + // Remove output directory if it already exists + if _, err := os.Stat(path.Join(tempDirectory, "output")); err == nil { + err := os.RemoveAll(path.Join(tempDirectory, "output")) + if err != nil { + return nil, err + } + } + + // Create new output directory + err = os.Mkdir(path.Join(tempDirectory, "output"), 0755) + if err != nil { + return nil, err + } + + // Change output directory owner + err = os.Chown(path.Join(tempDirectory, "output"), uid, gid) + if err != nil { + return nil, err + } + + // Execute package function in source.sh script and generate package file list + cmd = exec.Command("bash", "-c", + "set -a\n"+ // Source and export functions and variables in source.sh script + ". \"${BPM_WORKDIR}\"/source.sh\n"+ + "set +a\n"+ + "echo \"Running "+packageFunctionName+"() function\"\n"+ + "( cd \"$BPM_SOURCE\" && fakeroot -s \"$BPM_WORKDIR\"/fakeroot_file bash -e -c '"+packageFunctionName+"' ) || exit 1\n"+ // Run package() function + "fakeroot -i \"$BPM_WORKDIR\"/fakeroot_file find \"$BPM_OUTPUT\" -mindepth 1 -printf \"%P %#m %U %G %s\\n\" > \"$BPM_WORKDIR\"/pkg.files") // Create package file list + cmd.Dir = tempDirectory + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = env + if os.Getuid() == 0 { + cmd.SysProcAttr = &syscall.SysProcAttr{} + cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)} + } + err = cmd.Run() + if err != nil { + return nil, err + } + + // Create gzip-compressed archive for the package files + cmd = exec.Command("bash", "-c", "find output -printf \"%P\\n\" | fakeroot -i \"$BPM_WORKDIR\"/fakeroot_file tar -czf files.tar.gz --no-recursion -C output -T -") + cmd.Dir = tempDirectory + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = env + if os.Getuid() == 0 { + cmd.SysProcAttr = &syscall.SysProcAttr{} + cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)} + } + err = cmd.Run() + if err != nil { + return nil, fmt.Errorf("files.tar.gz archive could not be created: %s", err) + } + + // Clone source package info + pkgInfo := *pkg + + // Set package type to binary + pkgInfo.Type = "binary" + + // Set package architecture + if val, ok := compilationOptions["ARCH"]; ok { + pkgInfo.Arch = val + } else { + pkgInfo.Arch = GetArch() + } + + // Remove split package field + pkgInfo.SplitPackages = nil + + // Marshal package info + pkgInfoBytes, err := yaml.Marshal(pkgInfo) + if err != nil { + return nil, err + } + pkgInfoBytes = append(pkgInfoBytes, '\n') + + // Create pkg.info file + err = os.WriteFile(path.Join(tempDirectory, "pkg.info"), pkgInfoBytes, 0644) + if err != nil { + return nil, err + } + + // Change pkg.info file owner + err = os.Chown(path.Join(tempDirectory, "pkg.info"), uid, gid) + if err != nil { + return nil, err + } + + // Get files to include in BPM archive + bpmArchiveFiles := make([]string, 0) + bpmArchiveFiles = append(bpmArchiveFiles, "pkg.info", "pkg.files", "files.tar.gz") // Base files + bpmArchiveFiles = append(bpmArchiveFiles, packageScripts...) // Package scripts + + // Create final BPM archive + cmd = exec.Command("bash", "-c", "tar -cf final-archive.bpm --owner=0 --group=0 -C \"$BPM_WORKDIR\" "+strings.Join(bpmArchiveFiles, " ")) + cmd.Dir = tempDirectory + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + currentDir, err := os.Getwd() + if err != nil { + return nil, err + } + cmd.Env = append(env, "CURRENT_DIR="+currentDir) + if os.Getuid() == 0 { + cmd.SysProcAttr = &syscall.SysProcAttr{} + cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)} + } + err = cmd.Run() + if err != nil { + return nil, fmt.Errorf("BPM archive could not be created: %s", err) + } + + // Remove pkg.info file + err = os.Remove(path.Join(tempDirectory, "pkg.info")) + if err != nil { + return nil, err + } + + // Set output filename + outputFilename := path.Join(outputDirectory, fmt.Sprintf("%s-%s-%d-%s.bpm", pkgInfo.Name, pkgInfo.Version, pkgInfo.Revision, pkgInfo.Arch)) + + // Move final BPM archive + err = os.Rename(path.Join(tempDirectory, "final-archive.bpm"), outputFilename) + if err != nil { + return nil, err + } + + // Set final BPM archive owner + err = os.Chown(outputFilename, os.Getuid(), os.Getgid()) + if err != nil { + return nil, err + } + + outputBpmPackages[pkgInfo.Name] = outputFilename + } + + return outputBpmPackages, nil +} + +func readCompilationOptionsFile() (options map[string]string, err error) { + // Initialize options map + options = make(map[string]string) + + // Check if file compilation options file exists + stat, err := os.Stat(".compilation-options") + if err != nil { + return nil, nil + } + + // Ensure it is a regular file + if !stat.Mode().IsRegular() { + return nil, fmt.Errorf("%s is not a regular file", stat.Name()) + } + + // Read file data + data, err := os.ReadFile(stat.Name()) + if err != nil { + return nil, err + } + + for _, line := range strings.Split(string(data), "\n") { + // Trim line + line = strings.TrimSpace(line) + + // Skip empty lines + if line == "" { + continue + } + + // Split line + split := strings.SplitN(line, "=", 2) + + // Throw error if line isn't valid + if len(split) < 2 { + return nil, fmt.Errorf("invalid line in compilation-options file: '%s'", line) + } + + options[split[0]] = split[1] + } + + return options, nil +} diff --git a/src/bpmlib/config.go b/src/bpmlib/config.go new file mode 100644 index 0000000..34a4762 --- /dev/null +++ b/src/bpmlib/config.go @@ -0,0 +1,63 @@ +package bpmlib + +import ( + "os" + + "gopkg.in/yaml.v3" +) + +type MainBPMConfigStruct struct { + IgnorePackages []string `yaml:"ignore_packages"` + CleanupMakeDependencies bool `yaml:"cleanup_make_dependencies"` + Databases []*BPMDatabase `yaml:"databases"` +} + +type CompilationBPMConfigStruct struct { + PrivilegeEscalatorCmd string `yaml:"privilege_escalator_cmd"` + CompilationEnvironment []string `yaml:"compilation_env"` +} + +var MainBPMConfig MainBPMConfigStruct +var CompilationBPMConfig CompilationBPMConfigStruct + +func ReadConfig() (err error) { + var file *os.File + + // Set default config options + MainBPMConfig = MainBPMConfigStruct{ + CleanupMakeDependencies: true, + } + + // Read main BPM config + file, err = os.Open("/etc/bpm.conf") + if err != nil { + return err + } + err = yaml.NewDecoder(file).Decode(&MainBPMConfig) + if err != nil { + return err + } + file.Close() + + // Read compilation BPM config + if _, err := os.Stat("/etc/bpm-compilation.conf"); err == nil { + file, err = os.Open("/etc/bpm-compilation.conf") + if err != nil { + return err + } + err = yaml.NewDecoder(file).Decode(&CompilationBPMConfig) + if err != nil { + return err + } + file.Close() + } + + // Remove disabled databases from memory + for i := len(MainBPMConfig.Databases) - 1; i >= 0; i-- { + if MainBPMConfig.Databases[i].Disabled != nil && *MainBPMConfig.Databases[i].Disabled { + MainBPMConfig.Databases = append(MainBPMConfig.Databases[:i], MainBPMConfig.Databases[i+1:]...) + } + } + + return nil +} diff --git a/src/bpmlib/databases.go b/src/bpmlib/databases.go new file mode 100644 index 0000000..4d574c6 --- /dev/null +++ b/src/bpmlib/databases.go @@ -0,0 +1,282 @@ +package bpmlib + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "strings" + + "gopkg.in/yaml.v3" +) + +type BPMDatabase struct { + Name string `yaml:"name"` + Source string `yaml:"source"` + Disabled *bool `yaml:"disabled"` + Entries map[string]*BPMDatabaseEntry + VirtualPackages map[string][]string +} + +type BPMDatabaseEntry struct { + Info *PackageInfo `yaml:"info"` + Download string `yaml:"download"` + DownloadSize uint64 `yaml:"download_size"` + InstalledSize uint64 `yaml:"installed_size"` + Database *BPMDatabase +} + +func (db *BPMDatabase) ContainsPackage(pkg string) bool { + _, ok := db.Entries[pkg] + return ok +} + +func (db *BPMDatabase) ReadLocalDatabase() error { + dbFile := "/var/lib/bpm/databases/" + db.Name + ".bpmdb" + if _, err := os.Stat(dbFile); err != nil { + return nil + } + + bytes, err := os.ReadFile(dbFile) + if err != nil { + return err + } + + data := string(bytes) + for _, b := range strings.Split(data, "---") { + entry := BPMDatabaseEntry{ + Info: &PackageInfo{ + Name: "", + Description: "", + Version: "", + Revision: 1, + Url: "", + License: "", + Arch: "", + Type: "", + Keep: make([]string, 0), + Depends: make([]string, 0), + MakeDepends: make([]string, 0), + OptionalDepends: make([]string, 0), + Conflicts: make([]string, 0), + Provides: make([]string, 0), + }, + Download: "", + DownloadSize: 0, + InstalledSize: 0, + Database: db, + } + err := yaml.Unmarshal([]byte(b), &entry) + if err != nil { + return err + } + + // Create database entries + if entry.Info.IsSplitPackage() { + for _, splitPkg := range entry.Info.SplitPackages { + // Turn split package into json data + splitPkgJson, err := yaml.Marshal(splitPkg) + if err != nil { + return err + } + + // Clone all main package fields onto split package + splitPkgClone := *entry.Info + + // Set split package field of split package to nil + splitPkgClone.SplitPackages = nil + + // Unmarshal json data back to struct + err = yaml.Unmarshal(splitPkgJson, &splitPkgClone) + if err != nil { + return err + } + + // Force set split package version, revision and URL + splitPkgClone.Version = entry.Info.Version + splitPkgClone.Revision = entry.Info.Revision + splitPkgClone.Url = entry.Info.Url + + // Create entry for split package + db.Entries[splitPkg.Name] = &BPMDatabaseEntry{ + Info: &splitPkgClone, + Download: entry.Download, + DownloadSize: entry.DownloadSize, + InstalledSize: 0, + Database: db, + } + + // Add virtual packages to database + for _, p := range splitPkg.Provides { + db.VirtualPackages[p] = append(db.VirtualPackages[p], splitPkg.Name) + } + } + } else { + // Create entry for package + db.Entries[entry.Info.Name] = &entry + + // Add virtual packages to database + for _, p := range entry.Info.Provides { + db.VirtualPackages[p] = append(db.VirtualPackages[p], entry.Info.Name) + } + } + + } + + return nil +} + +func (db *BPMDatabase) SyncLocalDatabaseFile() error { + dbFile := "/var/lib/bpm/databases/" + db.Name + ".bpmdb" + + // Get URL to database + u, err := url.JoinPath(db.Source, "database.bpmdb") + if err != nil { + return err + } + + // Retrieve data from URL + resp, err := http.Get(u) + if err != nil { + return err + } + defer resp.Body.Close() + + // Load data into byte buffer + buffer, err := io.ReadAll(resp.Body) + + // Unmarshal data to ensure it is a valid BPM database + err = yaml.Unmarshal(buffer, &BPMDatabase{}) + if err != nil { + return fmt.Errorf("could not decode database: %s", err) + } + + // Create parent directories to database file + err = os.MkdirAll(path.Dir(dbFile), 0755) + if err != nil { + return err + } + + // Create file and save database data + out, err := os.Create(dbFile) + if err != nil { + return err + } + defer out.Close() + + _, err = out.Write(buffer) + + return nil +} + +func ReadLocalDatabaseFiles() (err error) { + for _, db := range MainBPMConfig.Databases { + // Initialize struct values + db.Entries = make(map[string]*BPMDatabaseEntry) + db.VirtualPackages = make(map[string][]string) + + // Read database + err = db.ReadLocalDatabase() + if err != nil { + return err + } + } + + return nil +} + +func GetDatabase(name string) *BPMDatabase { + for _, db := range MainBPMConfig.Databases { + if db.Name == name { + return db + } + } + return nil +} + +func GetDatabaseEntry(str string) (*BPMDatabaseEntry, *BPMDatabase, error) { + split := strings.Split(str, "/") + if len(split) == 1 { + pkgName := strings.TrimSpace(split[0]) + if pkgName == "" { + return nil, nil, errors.New("could not find database entry for this package") + } + for _, db := range MainBPMConfig.Databases { + if db.ContainsPackage(pkgName) { + return db.Entries[pkgName], db, nil + } + } + return nil, nil, errors.New("could not find database entry for this package") + } else if len(split) == 2 { + dbName := strings.TrimSpace(split[0]) + pkgName := strings.TrimSpace(split[1]) + if dbName == "" || pkgName == "" { + return nil, nil, errors.New("could not find database entry for this package") + } + db := GetDatabase(dbName) + if db == nil || !db.ContainsPackage(pkgName) { + return nil, nil, errors.New("could not find database entry for this package") + } + return db.Entries[pkgName], db, nil + } else { + return nil, nil, errors.New("could not find database entry for this package") + } +} + +func FindReplacement(pkg string) *BPMDatabaseEntry { + for _, db := range MainBPMConfig.Databases { + for _, entry := range db.Entries { + for _, replaced := range entry.Info.Replaces { + if replaced == pkg { + return entry + } + } + } + } + + return nil +} + +func ResolveVirtualPackage(vpkg string) *BPMDatabaseEntry { + for _, db := range MainBPMConfig.Databases { + if v, ok := db.VirtualPackages[vpkg]; ok { + for _, pkg := range v { + return db.Entries[pkg] + } + } + } + + return nil +} + +func (db *BPMDatabase) FetchPackage(pkg string) (string, error) { + if !db.ContainsPackage(pkg) { + return "", errors.New("could not fetch package '" + pkg + "'") + } + entry := db.Entries[pkg] + URL, err := url.JoinPath(db.Source, entry.Download) + if err != nil { + return "", err + } + resp, err := http.Get(URL) + if err != nil { + return "", err + } + defer resp.Body.Close() + + err = os.MkdirAll("/var/cache/bpm/fetched/", 0755) + if err != nil { + return "", err + } + out, err := os.Create("/var/cache/bpm/fetched/" + path.Base(entry.Download)) + if err != nil { + return "", err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return "/var/cache/bpm/fetched/" + path.Base(entry.Download), nil +} diff --git a/src/bpmlib/dependencies.go b/src/bpmlib/dependencies.go new file mode 100644 index 0000000..9cbb9c8 --- /dev/null +++ b/src/bpmlib/dependencies.go @@ -0,0 +1,225 @@ +package bpmlib + +import ( + "errors" + "fmt" + "slices" +) + +type pkgInstallationReason struct { + PkgName string + InstallationReason InstallationReason +} + +func (pkgInfo *PackageInfo) GetDependencies(includeMakeDepends, includeOptionalDepends bool) []pkgInstallationReason { + allDepends := make([]pkgInstallationReason, 0) + + for _, depend := range pkgInfo.Depends { + if !slices.ContainsFunc(allDepends, func(p pkgInstallationReason) bool { + return p.PkgName == depend + }) { + allDepends = append(allDepends, pkgInstallationReason{ + PkgName: depend, + InstallationReason: InstallationReasonDependency, + }) + } + } + if includeOptionalDepends { + for _, depend := range pkgInfo.OptionalDepends { + if !slices.ContainsFunc(allDepends, func(p pkgInstallationReason) bool { + return p.PkgName == depend + }) { + allDepends = append(allDepends, pkgInstallationReason{ + PkgName: depend, + InstallationReason: InstallationReasonDependency, + }) + } + } + } + if includeMakeDepends { + for _, depend := range pkgInfo.MakeDepends { + if !slices.ContainsFunc(allDepends, func(p pkgInstallationReason) bool { + return p.PkgName == depend + }) { + allDepends = append(allDepends, pkgInstallationReason{ + PkgName: depend, + InstallationReason: InstallationReasonMakeDependency, + }) + } + } + } + return allDepends +} + +func (pkgInfo *PackageInfo) GetAllDependencies(includeMakeDepends, includeOptionalDepends bool, rootDir string) (resolved []string) { + // Initialize slices + resolved = make([]string, 0) + unresolved := make([]string, 0) + + // Call unexported function + pkgInfo.getAllDependencies(&resolved, &unresolved, includeMakeDepends, includeOptionalDepends, rootDir) + + return resolved +} + +func (pkgInfo *PackageInfo) getAllDependencies(resolved *[]string, unresolved *[]string, includeMakeDepends, includeOptionalDepends bool, rootDir string) { + // Add current package name to unresolved slice + *unresolved = append(*unresolved, pkgInfo.Name) + + // Loop through all dependencies + for _, pkgIR := range pkgInfo.GetDependencies(includeMakeDepends, includeOptionalDepends) { + depend := pkgIR.PkgName + + if isVirtual, p := IsVirtualPackage(depend, rootDir); isVirtual { + depend = p + } + + if !slices.Contains(*resolved, depend) { + // Add current dependency to resolved slice when circular dependency is detected + if slices.Contains(*unresolved, depend) { + if !slices.Contains(*resolved, depend) { + *resolved = append(*resolved, depend) + } + continue + } + + dependInfo := GetPackageInfo(depend, rootDir) + + if dependInfo != nil { + dependInfo.getAllDependencies(resolved, unresolved, includeMakeDepends, includeOptionalDepends, rootDir) + } + } + } + if !slices.Contains(*resolved, pkgInfo.Name) { + *resolved = append(*resolved, pkgInfo.Name) + } + *unresolved = stringSliceRemove(*unresolved, pkgInfo.Name) +} + +func ResolveAllPackageDependenciesFromDatabases(pkgInfo *PackageInfo, checkMake, checkOptional, ignoreInstalled, verbose bool, rootDir string) (resolved []pkgInstallationReason, unresolved []string) { + // Initialize slices + resolved = make([]pkgInstallationReason, 0) + unresolved = make([]string, 0) + + // Call unexported function + resolvePackageDependenciesFromDatabase(&resolved, &unresolved, pkgInfo, checkMake, checkOptional, ignoreInstalled, verbose, rootDir) + + // Remove main package from unresolved slice + unresolved = stringSliceRemove(unresolved, pkgInfo.Name) + + return resolved, unresolved +} + +func resolvePackageDependenciesFromDatabase(resolved *[]pkgInstallationReason, unresolved *[]string, pkgInfo *PackageInfo, checkMake, checkOptional, ignoreInstalled, verbose bool, rootDir string) { + // Add current package name to unresolved slice + *unresolved = append(*unresolved, pkgInfo.Name) + + // Loop through all dependencies + for _, pkgIR := range pkgInfo.GetDependencies(pkgInfo.Type == "source", checkOptional) { + // Skip dependency if it has already been resolved + if slices.ContainsFunc(*resolved, func(p pkgInstallationReason) bool { + return p.PkgName == pkgIR.PkgName + }) { + continue + } + + // Add current dependency to resolved slice when circular dependency is detected + if slices.Contains(*unresolved, pkgIR.PkgName) { + if verbose { + fmt.Printf("Circular dependency was detected (%s -> %s). Installing %s first\n", pkgInfo.Name, pkgIR.PkgName, pkgIR.PkgName) + } + + *resolved = append(*resolved, pkgInstallationReason{ + PkgName: pkgIR.PkgName, + InstallationReason: pkgIR.InstallationReason, + }) + continue + } + + // Skip dependency if it is already installed or provided + if ignoreInstalled && IsPackageProvided(pkgIR.PkgName, rootDir) { + continue + } + + // Get database entry for dependency + var err error + var entry *BPMDatabaseEntry + entry, _, err = GetDatabaseEntry(pkgIR.PkgName) + if err != nil { + if entry = ResolveVirtualPackage(pkgIR.PkgName); entry == nil { + if !slices.Contains(*unresolved, pkgIR.PkgName) { + *unresolved = append(*unresolved, pkgIR.PkgName) + } + continue + } + } + + // Resolve the dependencies of this dependency + resolvePackageDependenciesFromDatabase(resolved, unresolved, entry.Info, checkMake, checkOptional, ignoreInstalled, verbose, rootDir) + + // Move dependency from the unresolved slice to the resolved slice + if !slices.ContainsFunc(*resolved, func(p pkgInstallationReason) bool { + return p.PkgName == entry.Info.Name + }) { + *resolved = append(*resolved, pkgInstallationReason{ + PkgName: entry.Info.Name, + InstallationReason: pkgIR.InstallationReason, + }) + } + *unresolved = stringSliceRemove(*unresolved, entry.Info.Name) + } +} + +func GetPackageDependants(pkgName string, rootDir string) ([]string, error) { + ret := make([]string, 0) + + // Get BPM package + pkg := GetPackage(pkgName, rootDir) + if pkg == nil { + return nil, errors.New("package not found: " + pkgName) + } + + // Get installed package names + pkgs, err := GetInstalledPackages(rootDir) + if err != nil { + return nil, errors.New("could not get installed packages") + } + + // Loop through all installed packages + for _, installedPkgName := range pkgs { + // Get installed BPM package + installedPkg := GetPackage(installedPkgName, rootDir) + if installedPkg == nil { + return nil, errors.New("package not found: " + installedPkgName) + } + + // Skip iteration if comparing the same packages + if installedPkg.PkgInfo.Name == pkgName { + continue + } + + // Get installed package dependencies + dependencies := installedPkg.PkgInfo.GetDependencies(false, true) + + // Add installed package to list if its dependencies include pkgName + if slices.ContainsFunc(dependencies, func(p pkgInstallationReason) bool { + return p.PkgName == pkgName + }) { + ret = append(ret, installedPkgName) + continue + } + + // Loop through each virtual package + for _, vpkg := range pkg.PkgInfo.Provides { + // Add installed package to list if its dependencies contain a provided virtual package + if slices.ContainsFunc(dependencies, func(p pkgInstallationReason) bool { + return p.PkgName == vpkg + }) { + ret = append(ret, installedPkgName) + break + } + } + } + + return ret, nil +} diff --git a/src/bpmlib/errors.go b/src/bpmlib/errors.go new file mode 100644 index 0000000..8b46b94 --- /dev/null +++ b/src/bpmlib/errors.go @@ -0,0 +1,42 @@ +package bpmlib + +import ( + "fmt" + "strings" +) + +type PackageNotFoundErr struct { + packages []string +} + +func (e PackageNotFoundErr) Error() string { + return "The following packages were not found in any databases: " + strings.Join(e.packages, ", ") +} + +type DependencyNotFoundErr struct { + dependencies []string +} + +func (e DependencyNotFoundErr) Error() string { + return "The following dependencies were not found in any databases: " + strings.Join(e.dependencies, ", ") +} + +type PackageConflictErr struct { + pkg string + conflicts []string +} + +func (e PackageConflictErr) Error() string { + return fmt.Sprintf("Package (%s) is in conflict with the following packages: %s", e.pkg, strings.Join(e.conflicts, ", ")) + +} + +type PackageScriptErr struct { + err error + packageName string + packageScript string +} + +func (e PackageScriptErr) Error() string { + return fmt.Sprintf("could not execute package script (%s) for package (%s): %s", e.packageScript, e.packageName, e.err) +} diff --git a/src/bpmlib/general.go b/src/bpmlib/general.go new file mode 100644 index 0000000..d5e301a --- /dev/null +++ b/src/bpmlib/general.go @@ -0,0 +1,422 @@ +package bpmlib + +import ( + "errors" + "fmt" + "log" + "os" + "path" + "slices" + "strings" +) + +type ReinstallMethod uint8 + +const ( + ReinstallMethodNone ReinstallMethod = iota + ReinstallMethodSpecified ReinstallMethod = iota + ReinstallMethodAll ReinstallMethod = iota +) + +// InstallPackages installs the specified packages into the given root directory by fetching them from databases or directly from local bpm archives +func InstallPackages(rootDir string, forceInstallationReason InstallationReason, reinstallMethod ReinstallMethod, installOptionalDependencies, forceInstallation, verbose bool, packages ...string) (operation *BPMOperation, err error) { + + // Setup operation struct + operation = &BPMOperation{ + Actions: make([]OperationAction, 0), + UnresolvedDepends: make([]string, 0), + Changes: make(map[string]string), + RootDir: rootDir, + compiledPackages: make(map[string]string), + } + + // Resolve packages + pkgsNotFound := make([]string, 0) + for _, pkg := range packages { + if stat, err := os.Stat(pkg); err == nil && !stat.IsDir() { + bpmpkg, err := ReadPackage(pkg) + if err != nil { + return nil, fmt.Errorf("could not read package: %s", err) + } + + if bpmpkg.PkgInfo.Type == "source" && bpmpkg.PkgInfo.IsSplitPackage() { + for _, splitPkg := range bpmpkg.PkgInfo.SplitPackages { + if reinstallMethod == ReinstallMethodNone && IsPackageInstalled(splitPkg.Name, rootDir) && GetPackageInfo(splitPkg.Name, rootDir).GetFullVersion() == splitPkg.GetFullVersion() { + continue + } + + // Set package installation reason + installationReason := forceInstallationReason + if installationReason == InstallationReasonUnknown { + if IsPackageInstalled(splitPkg.Name, rootDir) { + installationReason = GetInstallationReason(splitPkg.Name, rootDir) + } else { + installationReason = InstallationReasonManual + } + } + + operation.AppendAction(&InstallPackageAction{ + File: pkg, + InstallationReason: installationReason, + BpmPackage: bpmpkg, + SplitPackageToInstall: splitPkg.Name, + }) + } + continue + } + + if reinstallMethod == ReinstallMethodNone && IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) && GetPackageInfo(bpmpkg.PkgInfo.Name, rootDir).GetFullVersion() == bpmpkg.PkgInfo.GetFullVersion() { + continue + } + + // Set package installation reason + installationReason := forceInstallationReason + if installationReason == InstallationReasonUnknown { + if IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) { + installationReason = GetInstallationReason(bpmpkg.PkgInfo.Name, rootDir) + } else { + installationReason = InstallationReasonManual + } + } + + operation.AppendAction(&InstallPackageAction{ + File: pkg, + InstallationReason: installationReason, + BpmPackage: bpmpkg, + }) + } else { + var entry *BPMDatabaseEntry + + if e, _, err := GetDatabaseEntry(pkg); err == nil { + entry = e + } else if isVirtual, p := IsVirtualPackage(pkg, rootDir); isVirtual { + entry, _, err = GetDatabaseEntry(p) + if err != nil { + pkgsNotFound = append(pkgsNotFound, pkg) + continue + } + } else if e := ResolveVirtualPackage(pkg); e != nil { + entry = e + } else { + pkgsNotFound = append(pkgsNotFound, pkg) + continue + } + if reinstallMethod == ReinstallMethodNone && IsPackageInstalled(entry.Info.Name, rootDir) && GetPackageInfo(entry.Info.Name, rootDir).GetFullVersion() == entry.Info.GetFullVersion() { + continue + } + + // Set package installation reason + installationReason := forceInstallationReason + if installationReason == InstallationReasonUnknown { + if IsPackageInstalled(entry.Info.Name, rootDir) { + installationReason = GetInstallationReason(entry.Info.Name, rootDir) + } else { + installationReason = InstallationReasonManual + } + } + + operation.AppendAction(&FetchPackageAction{ + InstallationReason: installationReason, + DatabaseEntry: entry, + }) + } + } + + // Return error if not all packages are found + if len(pkgsNotFound) != 0 { + return nil, PackageNotFoundErr{pkgsNotFound} + } + + // Resolve dependencies + err = operation.ResolveDependencies(reinstallMethod == ReinstallMethodAll, installOptionalDependencies, verbose) + if err != nil { + return nil, fmt.Errorf("could not resolve dependencies: %s", err) + } + if len(operation.UnresolvedDepends) != 0 { + if !forceInstallation { + return nil, DependencyNotFoundErr{operation.UnresolvedDepends} + } else if verbose { + log.Printf("Warning: %s", DependencyNotFoundErr{operation.UnresolvedDepends}) + } + } + + // Replace obsolete packages + operation.ReplaceObsoletePackages() + + // Check for conflicts + conflicts, err := operation.CheckForConflicts() + if err != nil { + return nil, fmt.Errorf("could not complete package conflict check: %s", err) + } + if len(conflicts) > 0 { + err = nil + for pkg, conflict := range conflicts { + err = errors.Join(err, PackageConflictErr{pkg, conflict}) + } + if !forceInstallation { + return nil, err + } else { + log.Printf("Warning: %s", err) + } + } + + // Check whether compiling source packages on different root directory + if rootDir != "/" { + sourcePackages := make([]string, 0) + for _, action := range operation.Actions { + switch action.(type) { + case *InstallPackageAction: + if action.(*InstallPackageAction).BpmPackage.PkgInfo.Type == "source" { + sourcePackages = append(sourcePackages, action.(*InstallPackageAction).BpmPackage.PkgInfo.Name) + } + case *FetchPackageAction: + if action.(*FetchPackageAction).DatabaseEntry.Info.Type == "source" { + sourcePackages = append(sourcePackages, action.(*FetchPackageAction).DatabaseEntry.Info.Name) + } + } + } + + // Return error if source packages are present in the operation + if len(sourcePackages) != 0 { + return nil, fmt.Errorf("cannot compile source packages in different root directory: %s", strings.Join(sourcePackages, ", ")) + } + } + + return operation, nil +} + +// RemovePackages removes the specified packages from the given root directory +func RemovePackages(rootDir string, removeUnusedPackagesOnly, cleanupDependencies bool, packages ...string) (operation *BPMOperation, err error) { + operation = &BPMOperation{ + Actions: make([]OperationAction, 0), + UnresolvedDepends: make([]string, 0), + Changes: make(map[string]string), + RootDir: rootDir, + compiledPackages: make(map[string]string), + } + + // Search for packages + for _, pkg := range packages { + bpmpkg := GetPackage(pkg, rootDir) + if bpmpkg == nil { + continue + } + operation.AppendAction(&RemovePackageAction{BpmPackage: bpmpkg}) + } + + // Do not remove packages which other packages depend on + if removeUnusedPackagesOnly { + err := operation.RemoveNeededPackages() + if err != nil { + return nil, fmt.Errorf("could not skip needed packages: %s", err) + } + } + + // Do package cleanup + if cleanupDependencies { + err := operation.Cleanup(true) + if err != nil { + return nil, fmt.Errorf("could not perform cleanup for operation: %s", err) + } + } + return operation, nil +} + +// CleanupPackages finds packages installed as dependencies which are no longer required by the rest of the system in the given root directory +func CleanupPackages(cleanupMakeDepends bool, rootDir string) (operation *BPMOperation, err error) { + operation = &BPMOperation{ + Actions: make([]OperationAction, 0), + UnresolvedDepends: make([]string, 0), + Changes: make(map[string]string), + RootDir: rootDir, + compiledPackages: make(map[string]string), + } + + // Do package cleanup + err = operation.Cleanup(cleanupMakeDepends) + if err != nil { + return nil, fmt.Errorf("could not perform cleanup for operation: %s", err) + } + + return operation, nil +} + +func CleanupCache(rootDir string, cleanupCompilationFiles, cleanupCompiledPackages, cleanupFetchedPackages, verbose bool) error { + if cleanupCompilationFiles { + globalCompilationCacheDir := path.Join(rootDir, "var/cache/bpm/compilation") + + // Ensure path exists and is a directory + if stat, err := os.Stat(globalCompilationCacheDir); err == nil && stat.IsDir() { + // Delete directory + if verbose { + log.Printf("Removing directory (%s)\n", globalCompilationCacheDir) + } + err = os.RemoveAll(globalCompilationCacheDir) + if err != nil { + return err + } + } + + // Get home directories of users in root directory + homeDirs, err := os.ReadDir(path.Join(rootDir, "home")) + if err != nil { + return err + } + + // Loop through all home directories + for _, homeDir := range homeDirs { + // Skip if not a directory + if !homeDir.IsDir() { + continue + } + + localCompilationDir := path.Join(rootDir, "home", homeDir.Name(), ".cache/bpm/compilation") + + // Ensure path exists and is a directory + if stat, err := os.Stat(localCompilationDir); err != nil || !stat.IsDir() { + continue + } + + // Delete directory + if verbose { + log.Printf("Removing directory (%s)\n", localCompilationDir) + } + err = os.RemoveAll(localCompilationDir) + if err != nil { + return err + } + } + } + + if cleanupCompiledPackages { + dirToRemove := path.Join(rootDir, "var/cache/bpm/compiled") + + // Ensure path exists and is a directory + if stat, err := os.Stat(dirToRemove); err == nil && stat.IsDir() { + // Delete directory + if verbose { + log.Printf("Removing directory (%s)\n", dirToRemove) + } + err = os.RemoveAll(dirToRemove) + if err != nil { + return err + } + } + } + + if cleanupFetchedPackages { + dirToRemove := path.Join(rootDir, "var/cache/bpm/fetched") + + // Ensure path exists and is a directory + if stat, err := os.Stat(dirToRemove); err == nil && stat.IsDir() { + // Delete directory + if verbose { + log.Printf("Removing directory (%s)\n", dirToRemove) + } + err = os.RemoveAll(dirToRemove) + if err != nil { + return err + } + } + } + + return nil +} + +// UpdatePackages fetches the newest versions of all installed packages from +func UpdatePackages(rootDir string, syncDatabase bool, installOptionalDependencies, forceInstallation, verbose bool) (operation *BPMOperation, err error) { + // Sync databases + if syncDatabase { + err := SyncDatabase(verbose) + if err != nil { + return nil, fmt.Errorf("could not sync local database: %s", err) + } + if verbose { + fmt.Println("All package databases synced successfully!") + } + + // Reload config and local databases + err = ReadConfig() + if err != nil { + return nil, fmt.Errorf("could not read BPM config: %s", err) + } + err = ReadLocalDatabaseFiles() + if err != nil { + return nil, fmt.Errorf("could not read local databases: %s", err) + } + } + + // Get installed packages and check for updates + pkgs, err := GetInstalledPackages(rootDir) + if err != nil { + return nil, fmt.Errorf("could not get installed packages: %s", err) + } + + operation = &BPMOperation{ + Actions: make([]OperationAction, 0), + UnresolvedDepends: make([]string, 0), + Changes: make(map[string]string), + RootDir: rootDir, + compiledPackages: make(map[string]string), + } + + // Search for packages + for _, pkg := range pkgs { + if slices.Contains(MainBPMConfig.IgnorePackages, pkg) { + continue + } + var entry *BPMDatabaseEntry + // Check if installed package can be replaced and install that instead + if e := FindReplacement(pkg); e != nil { + entry = e + } else if entry, _, err = GetDatabaseEntry(pkg); err != nil { + continue + } + + installedInfo := GetPackageInfo(pkg, rootDir) + if installedInfo == nil { + return nil, fmt.Errorf("could not get package info for package (%s)", pkg) + } else { + comparison := ComparePackageVersions(*entry.Info, *installedInfo) + if comparison > 0 { + operation.AppendAction(&FetchPackageAction{ + InstallationReason: GetInstallationReason(pkg, rootDir), + DatabaseEntry: entry, + }) + } + } + } + + // Check for new dependencies in updated packages + err = operation.ResolveDependencies(false, installOptionalDependencies, verbose) + if err != nil { + return nil, fmt.Errorf("could not resolve dependencies: %s", err) + } + if len(operation.UnresolvedDepends) != 0 { + if !forceInstallation { + return nil, DependencyNotFoundErr{operation.UnresolvedDepends} + } else if verbose { + log.Printf("Warning: %s", DependencyNotFoundErr{operation.UnresolvedDepends}) + } + } + + // Replace obsolete packages + operation.ReplaceObsoletePackages() + return operation, nil +} + +// SyncDatabase syncs all databases declared in /etc/bpm.conf +func SyncDatabase(verbose bool) (err error) { + for _, db := range MainBPMConfig.Databases { + if verbose { + fmt.Printf("Fetching package database file for database (%s)...\n", db.Name) + } + + err := db.SyncLocalDatabaseFile() + if err != nil { + return err + } + } + + return nil +} diff --git a/src/bpmlib/go.mod b/src/bpmlib/go.mod new file mode 100644 index 0000000..d0dabd5 --- /dev/null +++ b/src/bpmlib/go.mod @@ -0,0 +1,8 @@ +module git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib + +go 1.23 + +require ( + github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f + gopkg.in/yaml.v3 v3.0.1 +) diff --git a/src/bpmlib/go.sum b/src/bpmlib/go.sum new file mode 100644 index 0000000..8a51310 --- /dev/null +++ b/src/bpmlib/go.sum @@ -0,0 +1,6 @@ +github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f h1:xt29M2T6STgldg+WEP51gGePQCsQvklmP2eIhPIBK3g= +github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/bpmlib/hooks.go b/src/bpmlib/hooks.go new file mode 100644 index 0000000..d6e6d23 --- /dev/null +++ b/src/bpmlib/hooks.go @@ -0,0 +1,158 @@ +package bpmlib + +import ( + "errors" + "fmt" + "gopkg.in/yaml.v3" + "os" + "os/exec" + "path" + "path/filepath" + "slices" + "strings" + "syscall" +) + +type BPMHook struct { + SourcePath string + SourceContent string + TriggerOperations []string `yaml:"trigger_operations"` + TargetType string `yaml:"target_type"` + Targets []string `yaml:"targets"` + Depends []string `yaml:"depends"` + Run string `yaml:"run"` +} + +// createHook returns a BPMHook instance based on the content of the given string +func createHook(sourcePath string) (*BPMHook, error) { + // Read hook from source path + bytes, err := os.ReadFile(sourcePath) + if err != nil { + return nil, err + } + + // Create base hook structure + hook := &BPMHook{ + SourcePath: sourcePath, + SourceContent: string(bytes), + TriggerOperations: nil, + TargetType: "", + Targets: nil, + Depends: nil, + Run: "", + } + + // Unmarshal yaml string + err = yaml.Unmarshal(bytes, hook) + if err != nil { + return nil, err + } + + // Ensure hook is valid + if err := hook.IsValid(); err != nil { + return nil, err + } + + return hook, nil +} + +// IsValid ensures hook is valid +func (hook *BPMHook) IsValid() error { + ValidOperations := []string{"install", "upgrade", "remove"} + + // Return error if any trigger operation is not valid or none are given + if len(hook.TriggerOperations) == 0 { + return errors.New("no trigger operations specified") + } + for _, operation := range hook.TriggerOperations { + if !slices.Contains(ValidOperations, operation) { + return errors.New("trigger operation '" + operation + "' is not valid") + } + } + + if hook.TargetType != "package" && hook.TargetType != "path" { + return errors.New("target type '" + hook.TargetType + "' is not valid") + } + + if len(hook.Run) == 0 { + return errors.New("command to run is empty") + } + + // Return nil as hook is valid + return nil +} + +// Execute hook if all conditions are met +func (hook *BPMHook) Execute(packageChanges map[string]string, verbose bool, rootDir string) error { + // Check if package dependencies are met + installedPackages, err := GetInstalledPackages(rootDir) + if err != nil { + return err + } + + for _, depend := range hook.Depends { + if !slices.Contains(installedPackages, depend) { + return nil + } + } + + // Get modified files slice + modifiedFiles := make([]*PackageFileEntry, 0) + for pkg := range packageChanges { + if GetPackage(pkg, rootDir) != nil { + modifiedFiles = append(modifiedFiles, GetPackage(pkg, rootDir).PkgFiles...) + } + + } + + // Check if any targets are met + targetMet := false + for _, target := range hook.Targets { + if targetMet { + break + } + if hook.TargetType == "package" { + for change, operation := range packageChanges { + if target == change && slices.Contains(hook.TriggerOperations, operation) { + targetMet = true + break + } + } + } else { + glob, err := filepath.Glob(path.Join(rootDir, target)) + if err != nil { + return err + } + for _, change := range modifiedFiles { + if slices.Contains(glob, path.Join(rootDir, change.Path)) { + targetMet = true + break + } + } + } + } + if !targetMet { + return nil + } + + // Execute the command + splitCommand := strings.Split(hook.Run, " ") + cmd := exec.Command(splitCommand[0], splitCommand[1:]...) + // Setup subprocess environment + cmd.Dir = "/" + // Run hook in chroot if using the -R flag + if rootDir != "/" { + cmd.SysProcAttr = &syscall.SysProcAttr{Chroot: rootDir} + } + + if verbose { + fmt.Printf("Running hook (%s) with run command: %s\n", hook.SourcePath, strings.Join(splitCommand, " ")) + } + + err = cmd.Run() + if err != nil { + return err + } + + return nil +} diff --git a/src/bpmlib/installed_packages.go b/src/bpmlib/installed_packages.go new file mode 100644 index 0000000..dc295f1 --- /dev/null +++ b/src/bpmlib/installed_packages.go @@ -0,0 +1,243 @@ +package bpmlib + +import ( + "errors" + "fmt" + "os" + "path" + "slices" + "strconv" + "strings" +) + +var localPackageInformation map[string]map[string]*BPMPackage = make(map[string]map[string]*BPMPackage) + +func initializeLocalPackageInformation(rootDir string) (err error) { + // Return if information is already initialized + if _, ok := localPackageInformation[rootDir]; ok { + return nil + } + + tempPackageInformation := make(map[string]*BPMPackage) + + // Get path to installed package information directory + installedDir := path.Join(rootDir, "var/lib/bpm/installed/") + + // Get directory content + items, err := os.ReadDir(installedDir) + if os.IsNotExist(err) { + localPackageInformation[rootDir] = make(map[string]*BPMPackage) + return nil + } + if err != nil { + return err + } + + // Loop through each subdirectory + for _, item := range items { + // Skip if not a directory + if !item.IsDir() { + continue + } + + // Read package info + infoData, err := os.ReadFile(path.Join(installedDir, item.Name(), "info")) + if err != nil { + return err + } + info, err := ReadPackageInfo(string(infoData)) + if err != nil { + return err + } + + // Read package files + files := getPackageFiles(info.Name, rootDir) + + // Add package to slice + tempPackageInformation[info.Name] = &BPMPackage{ + PkgInfo: info, + PkgFiles: files, + } + } + + localPackageInformation[rootDir] = tempPackageInformation + return nil +} + +func GetInstalledPackages(rootDir string) (ret []string, err error) { + // Initialize local package information + err = initializeLocalPackageInformation(rootDir) + if err != nil { + return nil, err + } + + // Loop through each package and add it to slice + for _, bpmpkg := range localPackageInformation[rootDir] { + ret = append(ret, bpmpkg.PkgInfo.Name) + } + + return ret, nil +} + +func IsPackageInstalled(pkg, rootDir string) bool { + // Initialize local package information + err := initializeLocalPackageInformation(rootDir) + if err != nil { + return false + } + + if _, ok := localPackageInformation[rootDir][pkg]; !ok { + return false + } + return true +} + +func GetPackageInfo(pkg string, rootDir string) *PackageInfo { + // Get BPM package + bpmpkg := GetPackage(pkg, rootDir) + + // Return nil if not found + if bpmpkg == nil { + return nil + } + + return bpmpkg.PkgInfo +} + +func IsVirtualPackage(pkg, rootDir string) (bool, string) { + pkgs, err := GetInstalledPackages(rootDir) + if err != nil { + return false, "" + } + for _, p := range pkgs { + if p == pkg { + return false, "" + } + i := GetPackageInfo(p, rootDir) + if i == nil { + continue + } + if slices.Contains(i.Provides, pkg) { + return true, p + } + } + return false, "" +} + +func IsPackageProvided(pkg, rootDir string) bool { + pkgs, err := GetInstalledPackages(rootDir) + if err != nil { + return false + } + for _, p := range pkgs { + if p == pkg { + return true + } + i := GetPackageInfo(p, rootDir) + if i == nil { + continue + } + if slices.Contains(i.Provides, pkg) { + return true + } + } + return false +} + +func GetPackage(pkg, rootDir string) *BPMPackage { + err := initializeLocalPackageInformation(rootDir) + if err != nil { + return nil + } + + bpmpkg := localPackageInformation[rootDir][pkg] + + return bpmpkg +} + +func GetAllPackageFiles(rootDir string, excludePackages ...string) (map[string][]*BPMPackage, error) { + ret := make(map[string][]*BPMPackage) + + pkgNames, err := GetInstalledPackages(rootDir) + if err != nil { + return nil, err + } + + for _, pkgName := range pkgNames { + if slices.Contains(excludePackages, pkgName) { + continue + } + bpmpkg := GetPackage(pkgName, rootDir) + if bpmpkg == nil { + return nil, errors.New(fmt.Sprintf("could not get BPM package (%s)", pkgName)) + } + for _, entry := range bpmpkg.PkgFiles { + if _, ok := ret[entry.Path]; ok { + ret[entry.Path] = append(ret[entry.Path], bpmpkg) + } else { + ret[entry.Path] = []*BPMPackage{bpmpkg} + } + } + } + + return ret, nil +} + +func getPackageFiles(pkg, rootDir string) []*PackageFileEntry { + var pkgFiles []*PackageFileEntry + installedDir := path.Join(rootDir, "var/lib/bpm/installed/") + pkgDir := path.Join(installedDir, pkg) + files := path.Join(pkgDir, "files") + if _, err := os.Stat(installedDir); os.IsNotExist(err) { + return nil + } + if _, err := os.Stat(pkgDir); os.IsNotExist(err) { + return nil + } + bs, err := os.ReadFile(files) + if err != nil { + return nil + } + + for _, line := range strings.Split(string(bs), "\n") { + if strings.TrimSpace(line) == "" { + continue + } + stringEntry := strings.Split(strings.TrimSpace(line), " ") + if len(stringEntry) < 5 { + pkgFiles = append(pkgFiles, &PackageFileEntry{ + Path: strings.TrimSuffix(line, "/"), + OctalPerms: 0, + UserID: 0, + GroupID: 0, + SizeInBytes: 0, + }) + continue + } + uid, err := strconv.ParseInt(stringEntry[len(stringEntry)-4], 0, 32) + if err != nil { + return nil + } + octalPerms, err := strconv.ParseInt(stringEntry[len(stringEntry)-3], 0, 32) + if err != nil { + return nil + } + gid, err := strconv.ParseInt(stringEntry[len(stringEntry)-2], 0, 32) + if err != nil { + return nil + } + size, err := strconv.ParseUint(stringEntry[len(stringEntry)-1], 0, 64) + if err != nil { + return nil + } + pkgFiles = append(pkgFiles, &PackageFileEntry{ + Path: strings.TrimSuffix(strings.Join(stringEntry[:len(stringEntry)-4], " "), "/"), + OctalPerms: uint32(octalPerms), + UserID: int(uid), + GroupID: int(gid), + SizeInBytes: size, + }) + } + + return pkgFiles +} diff --git a/src/bpmlib/operations.go b/src/bpmlib/operations.go new file mode 100644 index 0000000..2e0b4a7 --- /dev/null +++ b/src/bpmlib/operations.go @@ -0,0 +1,621 @@ +package bpmlib + +import ( + "errors" + "fmt" + "log" + "os" + "path" + "slices" + "strings" +) + +type BPMOperation struct { + Actions []OperationAction + UnresolvedDepends []string + Changes map[string]string + RootDir string + compiledPackages map[string]string +} + +func (operation *BPMOperation) ActionsContainPackage(pkg string) bool { + for _, action := range operation.Actions { + if action.GetActionType() == "install" { + if action.(*InstallPackageAction).BpmPackage.PkgInfo.Name == pkg { + return true + } + } else if action.GetActionType() == "fetch" { + if action.(*FetchPackageAction).DatabaseEntry.Info.Name == pkg { + return true + } + } else if action.GetActionType() == "remove" { + if action.(*RemovePackageAction).BpmPackage.PkgInfo.Name == pkg { + return true + } + } + } + return false +} + +func (operation *BPMOperation) AppendAction(action OperationAction) { + operation.InsertActionAt(len(operation.Actions), action) +} + +func (operation *BPMOperation) InsertActionAt(index int, action OperationAction) { + if len(operation.Actions) == index { // nil or empty slice or after last element + operation.Actions = append(operation.Actions, action) + } else { + operation.Actions = append(operation.Actions[:index+1], operation.Actions[index:]...) // index < len(a) + operation.Actions[index] = action + } + + if action.GetActionType() == "install" { + pkgInfo := action.(*InstallPackageAction).BpmPackage.PkgInfo + if !IsPackageInstalled(pkgInfo.Name, operation.RootDir) { + operation.Changes[pkgInfo.Name] = "install" + } else { + operation.Changes[pkgInfo.Name] = "upgrade" + } + } else if action.GetActionType() == "fetch" { + pkgInfo := action.(*FetchPackageAction).DatabaseEntry.Info + if !IsPackageInstalled(pkgInfo.Name, operation.RootDir) { + operation.Changes[pkgInfo.Name] = "install" + } else { + operation.Changes[pkgInfo.Name] = "upgrade" + } + } else if action.GetActionType() == "remove" { + operation.Changes[action.(*RemovePackageAction).BpmPackage.PkgInfo.Name] = "remove" + } +} + +func (operation *BPMOperation) RemoveAction(pkg, actionType string) { + operation.Actions = slices.DeleteFunc(operation.Actions, func(a OperationAction) bool { + if a.GetActionType() != actionType { + return false + } + if a.GetActionType() == "install" { + return a.(*InstallPackageAction).BpmPackage.PkgInfo.Name == pkg + } else if a.GetActionType() == "fetch" { + return a.(*FetchPackageAction).DatabaseEntry.Info.Name == pkg + } else if a.GetActionType() == "remove" { + return a.(*RemovePackageAction).BpmPackage.PkgInfo.Name == pkg + } + return false + }) +} + +func (operation *BPMOperation) GetTotalDownloadSize() uint64 { + var ret uint64 = 0 + for _, action := range operation.Actions { + if action.GetActionType() == "fetch" { + ret += action.(*FetchPackageAction).DatabaseEntry.DownloadSize + } + } + return ret +} + +func (operation *BPMOperation) GetTotalInstalledSize() uint64 { + var ret uint64 = 0 + for _, action := range operation.Actions { + if action.GetActionType() == "install" { + ret += action.(*InstallPackageAction).BpmPackage.GetInstalledSize() + } else if action.GetActionType() == "fetch" { + ret += action.(*FetchPackageAction).DatabaseEntry.InstalledSize + } + } + return ret +} + +func (operation *BPMOperation) GetFinalActionSize(rootDir string) int64 { + var ret int64 = 0 + for _, action := range operation.Actions { + if action.GetActionType() == "install" { + ret += int64(action.(*InstallPackageAction).BpmPackage.GetInstalledSize()) + if IsPackageInstalled(action.(*InstallPackageAction).BpmPackage.PkgInfo.Name, rootDir) { + ret -= int64(GetPackage(action.(*InstallPackageAction).BpmPackage.PkgInfo.Name, rootDir).GetInstalledSize()) + } + } else if action.GetActionType() == "fetch" { + ret += int64(action.(*FetchPackageAction).DatabaseEntry.InstalledSize) + } else if action.GetActionType() == "remove" { + ret -= int64(action.(*RemovePackageAction).BpmPackage.GetInstalledSize()) + } + } + return ret +} + +func (operation *BPMOperation) ResolveDependencies(reinstallDependencies, installOptionalDependencies, verbose bool) error { + pos := 0 + for _, value := range slices.Clone(operation.Actions) { + var pkgInfo *PackageInfo + if value.GetActionType() == "install" { + action := value.(*InstallPackageAction) + pkgInfo = action.BpmPackage.PkgInfo + } else if value.GetActionType() == "fetch" { + action := value.(*FetchPackageAction) + pkgInfo = action.DatabaseEntry.Info + } else { + pos++ + continue + } + + resolved, unresolved := ResolveAllPackageDependenciesFromDatabases(pkgInfo, pkgInfo.Type == "source", installOptionalDependencies, !reinstallDependencies, verbose, operation.RootDir) + + operation.UnresolvedDepends = append(operation.UnresolvedDepends, unresolved...) + + for _, resolvedPkg := range resolved { + if !operation.ActionsContainPackage(resolvedPkg.PkgName) && resolvedPkg.PkgName != pkgInfo.Name { + if !reinstallDependencies && IsPackageInstalled(resolvedPkg.PkgName, operation.RootDir) { + continue + } + entry, _, err := GetDatabaseEntry(resolvedPkg.PkgName) + if err != nil { + return errors.New("could not get database entry for package (" + resolvedPkg.PkgName + ")") + } + operation.InsertActionAt(pos, &FetchPackageAction{ + InstallationReason: resolvedPkg.InstallationReason, + DatabaseEntry: entry, + }) + pos++ + } + } + pos++ + } + + return nil +} + +func (operation *BPMOperation) RemoveNeededPackages() error { + removeActions := make(map[string]*RemovePackageAction) + for _, action := range slices.Clone(operation.Actions) { + if action.GetActionType() == "remove" { + removeActions[action.(*RemovePackageAction).BpmPackage.PkgInfo.Name] = action.(*RemovePackageAction) + } + } + + for pkg, action := range removeActions { + dependants, err := GetPackageDependants(action.BpmPackage.PkgInfo.Name, operation.RootDir) + if err != nil { + return errors.New("could not get dependant packages for package (" + pkg + ")") + } + dependants = slices.DeleteFunc(dependants, func(d string) bool { + if _, ok := removeActions[d]; ok { + return true + } + return false + }) + if len(dependants) != 0 { + operation.RemoveAction(pkg, action.GetActionType()) + } + } + + return nil +} + +func (operation *BPMOperation) Cleanup(cleanupMakeDepends bool) error { + // Get all installed packages + installedPackageNames, err := GetInstalledPackages(operation.RootDir) + if err != nil { + return fmt.Errorf("could not get installed packages: %s", err) + } + installedPackages := make([]*PackageInfo, len(installedPackageNames)) + for i, value := range installedPackageNames { + bpmpkg := GetPackage(value, operation.RootDir) + if bpmpkg == nil { + return errors.New("could not find installed package (" + value + ")") + } + installedPackages[i] = bpmpkg.PkgInfo + } + + // Get packages to remove + removeActions := make(map[string]*RemovePackageAction) + for _, action := range slices.Clone(operation.Actions) { + if action.GetActionType() == "remove" { + removeActions[action.(*RemovePackageAction).BpmPackage.PkgInfo.Name] = action.(*RemovePackageAction) + } + } + + // Get manually installed packages, resolve all their dependencies and add them to the keepPackages slice + keepPackages := make([]string, 0) + for _, pkg := range slices.Clone(installedPackages) { + if GetInstallationReason(pkg.Name, operation.RootDir) != InstallationReasonManual { + continue + } + + // Do not resolve dependencies or add package to keepPackages slice if package removal action exists for it + if _, ok := removeActions[pkg.Name]; ok { + continue + } + + keepPackages = append(keepPackages, pkg.Name) + resolved := pkg.GetAllDependencies(!cleanupMakeDepends, true, operation.RootDir) + for _, value := range resolved { + if !slices.Contains(keepPackages, value) { + keepPackages = append(keepPackages, value) + } + } + } + + // Get all installed packages that are not in the keepPackages slice and add them to the BPM operation + for _, pkg := range installedPackageNames { + // Do not add package removal action if there already is one + if _, ok := removeActions[pkg]; ok { + continue + } + if !slices.Contains(keepPackages, pkg) { + bpmpkg := GetPackage(pkg, operation.RootDir) + if bpmpkg == nil { + return errors.New("Error: could not find installed package (" + pkg + ")") + } + operation.Actions = append(operation.Actions, &RemovePackageAction{BpmPackage: bpmpkg}) + } + } + + return nil +} + +func (operation *BPMOperation) ReplaceObsoletePackages() { + for _, value := range slices.Clone(operation.Actions) { + var pkgInfo *PackageInfo + if value.GetActionType() == "install" { + action := value.(*InstallPackageAction) + pkgInfo = action.BpmPackage.PkgInfo + + } else if value.GetActionType() == "fetch" { + action := value.(*FetchPackageAction) + pkgInfo = action.DatabaseEntry.Info + } else { + continue + } + + for _, r := range pkgInfo.Replaces { + if bpmpkg := GetPackage(r, operation.RootDir); bpmpkg != nil && !operation.ActionsContainPackage(bpmpkg.PkgInfo.Name) { + operation.InsertActionAt(0, &RemovePackageAction{ + BpmPackage: bpmpkg, + }) + } + } + } +} + +func (operation *BPMOperation) CheckForConflicts() (map[string][]string, error) { + conflicts := make(map[string][]string) + installedPackages, err := GetInstalledPackages(operation.RootDir) + if err != nil { + return nil, err + } + allPackages := make([]*PackageInfo, len(installedPackages)) + for i, value := range installedPackages { + bpmpkg := GetPackage(value, operation.RootDir) + if bpmpkg == nil { + return nil, fmt.Errorf("could not find installed package (%s)", value) + } + allPackages[i] = bpmpkg.PkgInfo + } + + // Add all new packages to the allPackages slice + for _, value := range slices.Clone(operation.Actions) { + if value.GetActionType() == "install" { + action := value.(*InstallPackageAction) + pkgInfo := action.BpmPackage.PkgInfo + allPackages = append(allPackages, pkgInfo) + } else if value.GetActionType() == "fetch" { + action := value.(*FetchPackageAction) + pkgInfo := action.DatabaseEntry.Info + allPackages = append(allPackages, pkgInfo) + } else if value.GetActionType() == "remove" { + action := value.(*RemovePackageAction) + pkgInfo := action.BpmPackage.PkgInfo + for i := len(allPackages) - 1; i >= 0; i-- { + info := allPackages[i] + if info.Name == pkgInfo.Name { + allPackages = append(allPackages[:i], allPackages[i+1:]...) + } + } + } + } + + for _, value := range allPackages { + for _, conflict := range value.Conflicts { + if slices.ContainsFunc(allPackages, func(info *PackageInfo) bool { + return info.Name == conflict + }) { + conflicts[value.Name] = append(conflicts[value.Name], conflict) + } + } + } + + return conflicts, nil +} + +func (operation *BPMOperation) ShowOperationSummary() { + if len(operation.Actions) == 0 { + fmt.Println("No action needs to be taken") + return + } + + for _, value := range operation.Actions { + var pkgInfo *PackageInfo + var installationReason = InstallationReasonUnknown + if value.GetActionType() == "install" { + installationReason = value.(*InstallPackageAction).InstallationReason + pkgInfo = value.(*InstallPackageAction).BpmPackage.PkgInfo + if value.(*InstallPackageAction).SplitPackageToInstall != "" { + pkgInfo = pkgInfo.GetSplitPackageInfo(value.(*InstallPackageAction).SplitPackageToInstall) + } + } else if value.GetActionType() == "fetch" { + installationReason = value.(*FetchPackageAction).InstallationReason + pkgInfo = value.(*FetchPackageAction).DatabaseEntry.Info + } else { + pkgInfo = value.(*RemovePackageAction).BpmPackage.PkgInfo + fmt.Printf("%s: %s (Remove)\n", pkgInfo.Name, pkgInfo.GetFullVersion()) + continue + } + + installedInfo := GetPackageInfo(pkgInfo.Name, operation.RootDir) + additionalInfo := "" + switch installationReason { + case InstallationReasonManual: + additionalInfo = "(Manual)" + case InstallationReasonDependency: + additionalInfo = "(Dependency)" + case InstallationReasonMakeDependency: + additionalInfo = "(Make dependency)" + default: + additionalInfo = "(Unknown)" + } + + if pkgInfo.Type == "source" { + additionalInfo += " (From Source)" + } + + if installedInfo == nil { + fmt.Printf("%s: %s (Install) %s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), additionalInfo) + } else { + comparison := ComparePackageVersions(*pkgInfo, *installedInfo) + if comparison < 0 { + fmt.Printf("%s: %s -> %s (Downgrade) %s\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), additionalInfo) + } else if comparison > 0 { + fmt.Printf("%s: %s -> %s (Upgrade) %s\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), additionalInfo) + } else { + fmt.Printf("%s: %s (Reinstall) %s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), additionalInfo) + } + } + } + + if operation.RootDir != "/" { + fmt.Println("Warning: Operating in " + operation.RootDir) + } + if operation.GetTotalDownloadSize() > 0 { + fmt.Printf("%s will be downloaded to complete this operation\n", unsignedBytesToHumanReadable(operation.GetTotalDownloadSize())) + } + if operation.GetFinalActionSize(operation.RootDir) > 0 { + fmt.Printf("A total of %s will be installed after the operation finishes\n", bytesToHumanReadable(operation.GetFinalActionSize(operation.RootDir))) + } else if operation.GetFinalActionSize(operation.RootDir) < 0 { + fmt.Printf("A total of %s will be freed after the operation finishes\n", strings.TrimPrefix(bytesToHumanReadable(operation.GetFinalActionSize(operation.RootDir)), "-")) + } +} + +func (operation *BPMOperation) RunHooks(verbose bool) error { + // Return if hooks directory does not exist + if stat, err := os.Stat(path.Join(operation.RootDir, "var/lib/bpm/hooks")); err != nil || !stat.IsDir() { + return nil + } + + // Get directory entries in hooks directory + dirEntries, err := os.ReadDir(path.Join(operation.RootDir, "var/lib/bpm/hooks")) + if err != nil { + return err + } + + // Find all hooks, validate and execute them + for _, entry := range dirEntries { + if entry.Type().IsRegular() && strings.HasSuffix(entry.Name(), ".bpmhook") { + hook, err := createHook(path.Join(operation.RootDir, "var/lib/bpm/hooks", entry.Name())) + if err != nil { + log.Printf("Error while reading hook (%s): %s", entry.Name(), err) + } + + err = hook.Execute(operation.Changes, verbose, operation.RootDir) + if err != nil { + log.Printf("Warning: could not execute hook (%s): %s\n", entry.Name(), err) + continue + } + } + } + + return nil +} + +func (operation *BPMOperation) Execute(verbose, force bool) (err error) { + // Fetch packages from databases + if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool { + return action.GetActionType() == "fetch" + }) { + fmt.Println("Fetching packages from available databases...") + + // Create map for fetched packages + fetchedPackages := make(map[string]string) + + for i, action := range operation.Actions { + if action.GetActionType() != "fetch" { + continue + } + + // Get database entry + entry := action.(*FetchPackageAction).DatabaseEntry + + // Create bpmpkg variable + var bpmpkg *BPMPackage + + // Check if package has already been fetched from download link + if _, ok := fetchedPackages[entry.Download]; !ok { + // Fetch package from database + fetchedPackage, err := entry.Database.FetchPackage(entry.Info.Name) + if err != nil { + return fmt.Errorf("could not fetch package (%s): %s\n", entry.Info.Name, err) + } + + // Read fetched package + bpmpkg, err = ReadPackage(fetchedPackage) + if err != nil { + return fmt.Errorf("could not fetch package (%s): %s\n", entry.Info.Name, err) + } + + // Add fetched package to map + fetchedPackages[entry.Download] = fetchedPackage + + fmt.Printf("Package (%s) was successfully fetched!\n", entry.Info.Name) + } else { + // Read fetched package + bpmpkg, err = ReadPackage(fetchedPackages[entry.Download]) + if err != nil { + return fmt.Errorf("could not read package (%s): %s\n", entry.Info.Name, err) + } + + fmt.Printf("Package (%s) was successfully fetched!\n", entry.Info.Name) + } + + if bpmpkg.PkgInfo.IsSplitPackage() { + operation.Actions[i] = &InstallPackageAction{ + File: fetchedPackages[entry.Download], + InstallationReason: action.(*FetchPackageAction).InstallationReason, + BpmPackage: bpmpkg, + SplitPackageToInstall: entry.Info.Name, + } + } else { + operation.Actions[i] = &InstallPackageAction{ + File: fetchedPackages[entry.Download], + InstallationReason: action.(*FetchPackageAction).InstallationReason, + BpmPackage: bpmpkg, + } + } + } + } + + // Determine words to be used for the following message + words := make([]string, 0) + if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool { + return action.GetActionType() == "install" + }) { + words = append(words, "Installing") + } + + if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool { + return action.GetActionType() == "remove" + }) { + words = append(words, "Removing") + } + + if len(words) == 0 { + return nil + } + fmt.Printf("%s packages...\n", strings.Join(words, "/")) + + // Installing/Removing packages from system + for _, action := range operation.Actions { + if action.GetActionType() == "remove" { + pkgInfo := action.(*RemovePackageAction).BpmPackage.PkgInfo + err := removePackage(pkgInfo.Name, verbose, operation.RootDir) + if err != nil { + return fmt.Errorf("could not remove package (%s): %s\n", pkgInfo.Name, err) + } + } else if action.GetActionType() == "install" { + value := action.(*InstallPackageAction) + fileToInstall := value.File + bpmpkg := value.BpmPackage + var err error + + // Compile package if type is 'source' + if bpmpkg.PkgInfo.Type == "source" { + // Get path to compiled package directory + compiledDir := path.Join(operation.RootDir, "/var/cache/bpm/compiled/") + + // Create compiled package directory if not exists + if _, err := os.Stat(compiledDir); err != nil { + err := os.MkdirAll(compiledDir, 0755) + if err != nil { + return err + } + } + + // Get package name to install + pkgNameToInstall := bpmpkg.PkgInfo.Name + if bpmpkg.PkgInfo.IsSplitPackage() { + pkgNameToInstall = value.SplitPackageToInstall + } + + // Compile source package if not compiled already + if _, ok := operation.compiledPackages[pkgNameToInstall]; !ok { + outputBpmPackages, err := CompileSourcePackage(value.File, compiledDir, false) + if err != nil { + return fmt.Errorf("could not compile source package (%s): %s\n", value.File, err) + } + + // Add compiled packages to slice + for pkgName, pkgFile := range outputBpmPackages { + operation.compiledPackages[pkgName] = pkgFile + } + } + + // Set values + fileToInstall = operation.compiledPackages[pkgNameToInstall] + bpmpkg, err = ReadPackage(fileToInstall) + if err != nil { + return fmt.Errorf("could not read package (%s): %s\n", fileToInstall, err) + } + } + + if value.InstallationReason != InstallationReasonManual { + err = installPackage(fileToInstall, operation.RootDir, verbose, true) + } else { + err = installPackage(fileToInstall, operation.RootDir, verbose, force) + } + if err != nil { + return fmt.Errorf("could not install package (%s): %s\n", bpmpkg.PkgInfo.Name, err) + } + + // Set installed package's installation reason + err = SetInstallationReason(bpmpkg.PkgInfo.Name, value.InstallationReason, operation.RootDir) + if err != nil { + return fmt.Errorf("could not set installation reason for package (%s): %s\n", value.BpmPackage.PkgInfo.Name, err) + } + + fmt.Printf("Package (%s) was successfully installed\n", bpmpkg.PkgInfo.Name) + } + } + fmt.Println("Operation complete!") + + return nil +} + +type OperationAction interface { + GetActionType() string +} + +type InstallPackageAction struct { + File string + InstallationReason InstallationReason + SplitPackageToInstall string + BpmPackage *BPMPackage +} + +func (action *InstallPackageAction) GetActionType() string { + return "install" +} + +type FetchPackageAction struct { + InstallationReason InstallationReason + DatabaseEntry *BPMDatabaseEntry +} + +func (action *FetchPackageAction) GetActionType() string { + return "fetch" +} + +type RemovePackageAction struct { + BpmPackage *BPMPackage +} + +func (action *RemovePackageAction) GetActionType() string { + return "remove" +} diff --git a/src/bpmlib/packages.go b/src/bpmlib/packages.go new file mode 100644 index 0000000..070072d --- /dev/null +++ b/src/bpmlib/packages.go @@ -0,0 +1,1044 @@ +package bpmlib + +import ( + "archive/tar" + "compress/gzip" + "errors" + "fmt" + "io" + "log" + "os" + "os/exec" + "path" + "slices" + "sort" + "strconv" + "strings" + "syscall" + + version "github.com/knqyf263/go-rpm-version" + "gopkg.in/yaml.v3" +) + +type BPMPackage struct { + PkgInfo *PackageInfo + PkgFiles []*PackageFileEntry +} + +type PackageInfo struct { + Name string `yaml:"name,omitempty"` + 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"` + Type string `yaml:"type,omitempty"` + Keep []string `yaml:"keep,omitempty"` + Depends []string `yaml:"depends,omitempty"` + MakeDepends []string `yaml:"make_depends,omitempty"` + OptionalDepends []string `yaml:"optional_depends,omitempty"` + Conflicts []string `yaml:"conflicts,omitempty"` + Replaces []string `yaml:"replaces,omitempty"` + Provides []string `yaml:"provides,omitempty"` + SplitPackages []*PackageInfo `yaml:"split_packages,omitempty"` +} + +type PackageFileEntry struct { + Path string + OctalPerms uint32 + UserID int + GroupID int + SizeInBytes uint64 +} + +func (pkg *BPMPackage) GetInstalledSize() uint64 { + var totalSize uint64 = 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 ComparePackageVersions(info1, info2 PackageInfo) int { + v1 := version.NewVersion(info1.GetFullVersion()) + v2 := version.NewVersion(info2.GetFullVersion()) + + return v1.Compare(v2) +} + +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) + defer file.Close() + if err != nil { + return nil, err + } + + 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.ParseUint(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 +} + +type packageOperation uint8 + +const ( + packageOperationInstall packageOperation = 0 + packageOperationUpdate = 1 + packageOperationRemove = 2 +) + +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 hook in chroot if using the -R flag + if rootDir != "/" { + cmd.SysProcAttr = &syscall.SysProcAttr{Chroot: rootDir} + } + // 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{ + Name: "", + Description: "", + Version: "", + Revision: 1, + Url: "", + License: "", + Arch: "", + Type: "", + Keep: make([]string, 0), + Depends: make([]string, 0), + MakeDepends: make([]string, 0), + OptionalDepends: make([]string, 0), + Conflicts: make([]string, 0), + Replaces: make([]string, 0), + Provides: make([]string, 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) + } + } + + // Setup split package information + for i, splitPkg := range pkgInfo.SplitPackages { + // Ensure split package contains a name + if splitPkg.Name == "" { + return nil, fmt.Errorf("invalid split package name: %s", splitPkg.Name) + } + + // Turn split package into json data + splitPkgJson, err := yaml.Marshal(splitPkg) + if err != nil { + return nil, err + } + + // Clone all main package fields onto split package + pkgInfoClone := *pkgInfo + pkgInfo.SplitPackages[i] = &pkgInfoClone + + // Set split package field of split package to nil + pkgInfo.SplitPackages[i].SplitPackages = nil + + // Unmarshal json data back to struct + err = yaml.Unmarshal(splitPkgJson, &pkgInfo.SplitPackages[i]) + if err != nil { + return nil, err + } + + // Force set split package version, revision + pkgInfo.SplitPackages[i].Version = pkgInfo.Version + pkgInfo.SplitPackages[i].Revision = pkgInfo.Revision + } + + return pkgInfo, nil +} + +func CreateReadableInfo(showArchitecture, showType, showPackageRelations, showInstallationReason bool, pkgInfo *PackageInfo, 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()) + ret = append(ret, "URL: "+pkgInfo.Url) + ret = append(ret, "License: "+pkgInfo.License) + if showArchitecture { + ret = append(ret, "Architecture: "+pkgInfo.Arch) + } + if showType { + ret = append(ret, "Type: "+pkgInfo.Type) + } + if showPackageRelations { + appendArray("Dependencies", pkgInfo.Depends) + if pkgInfo.Type == "source" { + appendArray("Make Dependencies", pkgInfo.MakeDepends) + } + appendArray("Optional dependencies", pkgInfo.OptionalDepends) + dependants, err := GetPackageDependants(pkgInfo.Name, rootDir) + if err == nil { + appendArray("Dependant packages", dependants) + } + 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 IsPackageInstalled(pkgInfo.Name, rootDir) && showInstallationReason { + 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 { + if !IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) { + 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) + } + } + seenHardlinks := make(map[string]string) + file, err := os.Open(filename) + if err != nil { + return err + } + + 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) + } + 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) + } + 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) + } + 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 + } + 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) + // 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() + } + + 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 + + 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) + } + + // Fetching and reversing package file entry list + fileEntries := GetPackage(pkg, rootDir).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 + } + + // Removing package files + for _, entry := range fileEntries { + 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 +} diff --git a/src/bpmlib/tarball.go b/src/bpmlib/tarball.go new file mode 100644 index 0000000..df9876d --- /dev/null +++ b/src/bpmlib/tarball.go @@ -0,0 +1,209 @@ +package bpmlib + +import ( + "archive/tar" + "errors" + "io" + "os" + "path" + "strings" +) + +type tarballFileReader struct { + tarReader *tar.Reader + file *os.File +} + +func listTarballContent(tarballPath string) (content []string, err error) { + file, err := os.Open(tarballPath) + 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 + } + + switch header.Typeflag { + case tar.TypeDir: + continue + default: + content = append(content, header.Name) + } + } + + return content, nil +} + +func readTarballFile(tarballPath, fileToExtract string) (*tarballFileReader, error) { + file, err := os.Open(tarballPath) + if err != nil { + return nil, err + } + + tr := tar.NewReader(file) + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + if header.Name == fileToExtract { + if header.Typeflag != tar.TypeReg { + return nil, errors.New("file to extract must be a regular file") + } + + return &tarballFileReader{ + tarReader: tr, + file: file, + }, nil + } + } + + return nil, errors.New("could not file in tarball") +} + +func extractTarballFile(tarballPath, fileToExtract string, workingDirectory string, uid, gid int) (err error) { + file, err := os.Open(tarballPath) + if err != nil { + return err + } + defer file.Close() + + tr := tar.NewReader(file) + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + // Skip if filename does not match + if header.Name != fileToExtract { + continue + } + + // Trim directory name from header name + header.Name = strings.Split(header.Name, "/")[len(strings.Split(header.Name, "/"))-1] + outputPath := path.Join(workingDirectory, header.Name) + + switch header.Typeflag { + case tar.TypeReg: + // Create file and set permissions + file, err = os.Create(outputPath) + if err != nil { + return err + } + err := file.Chmod(header.FileInfo().Mode()) + if err != nil { + return err + } + if uid >= 0 && gid >= 0 { + err = file.Chown(uid, gid) + if err != nil { + return err + } + } + + // Copy data to file + _, err = io.Copy(file, tr) + if err != nil { + return err + } + + // Close file + file.Close() + default: + continue + } + } + + return nil +} + +func extractTarballDirectory(tarballPath, directoryToExtract, workingDirectory string, uid, gid int) (err error) { + file, err := os.Open(tarballPath) + if err != nil { + return err + } + defer file.Close() + + tr := tar.NewReader(file) + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + if strings.HasPrefix(header.Name, directoryToExtract+"/") { + // Skip directory to extract + if strings.TrimRight(header.Name, "/") == workingDirectory { + continue + } + + // Trim directory name from header name + header.Name = strings.TrimPrefix(header.Name, directoryToExtract+"/") + outputPath := path.Join(workingDirectory, header.Name) + + switch header.Typeflag { + case tar.TypeDir: + // Create directory + err := os.MkdirAll(outputPath, 0755) + if err != nil { + return err + } + + // Set directory owner + if uid >= 0 && gid >= 0 { + err = os.Chown(outputPath, uid, gid) + if err != nil { + return err + } + } + case tar.TypeReg: + // Create file and set permissions + file, err = os.Create(outputPath) + if err != nil { + return err + } + err := file.Chmod(header.FileInfo().Mode()) + if err != nil { + return err + } + if uid >= 0 && gid >= 0 { + err = file.Chown(uid, gid) + if err != nil { + return err + } + } + + // Copy data to file + _, err = io.Copy(file, tr) + if err != nil { + return err + } + + // Close file + file.Close() + default: + continue + } + } + } + + return nil +} diff --git a/utils/general_utils.go b/src/bpmlib/utils.go similarity index 66% rename from utils/general_utils.go rename to src/bpmlib/utils.go index dd1e812..99275a0 100644 --- a/utils/general_utils.go +++ b/src/bpmlib/utils.go @@ -1,10 +1,8 @@ -package utils +package bpmlib import ( "fmt" - "io" "math" - "os" "syscall" ) @@ -12,7 +10,7 @@ func GetArch() string { uname := syscall.Utsname{} err := syscall.Uname(&uname) if err != nil { - return "" + return "unknown" } var byteString [65]byte @@ -23,29 +21,6 @@ func GetArch() string { return string(byteString[:indexLength]) } -func copyFileContents(src, dst string) (err error) { - in, err := os.Open(src) - if err != nil { - return - } - defer in.Close() - out, err := os.Create(dst) - if err != nil { - return - } - defer func() { - cerr := out.Close() - if err == nil { - err = cerr - } - }() - if _, err = io.Copy(out, in); err != nil { - return - } - err = out.Sync() - return -} - func stringSliceRemove(s []string, r string) []string { for i, v := range s { if v == r { @@ -55,7 +30,7 @@ func stringSliceRemove(s []string, r string) []string { return s } -func UnsignedBytesToHumanReadable(b uint64) string { +func unsignedBytesToHumanReadable(b uint64) string { bf := float64(b) for _, unit := range []string{"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"} { if math.Abs(bf) < 1024.0 { @@ -66,7 +41,7 @@ func UnsignedBytesToHumanReadable(b uint64) string { return fmt.Sprintf("%.1fYiB", bf) } -func BytesToHumanReadable(b int64) string { +func bytesToHumanReadable(b int64) string { bf := float64(b) for _, unit := range []string{"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"} { if math.Abs(bf) < 1024.0 { diff --git a/utils/config.go b/utils/config.go deleted file mode 100644 index 97edef6..0000000 --- a/utils/config.go +++ /dev/null @@ -1,49 +0,0 @@ -package utils - -import ( - "gopkg.in/yaml.v3" - "log" - "os" -) - -type BPMConfigStruct struct { - CompilationEnv []string `yaml:"compilation_env"` - SilentCompilation bool `yaml:"silent_compilation"` - BinaryOutputDir string `yaml:"binary_output_dir"` - CompilationDir string `yaml:"compilation_dir"` - Repositories []*Repository `yaml:"repositories"` -} - -var BPMConfig BPMConfigStruct - -func ReadConfig() { - if _, err := os.Stat("/etc/bpm.conf"); os.IsNotExist(err) { - log.Fatal(err) - } - bytes, err := os.ReadFile("/etc/bpm.conf") - if err != nil { - log.Fatal(err) - } - BPMConfig = BPMConfigStruct{ - CompilationEnv: make([]string, 0), - SilentCompilation: false, - BinaryOutputDir: "/var/lib/bpm/compiled/", - CompilationDir: "/var/tmp/", - } - err = yaml.Unmarshal(bytes, &BPMConfig) - if err != nil { - log.Fatal(err) - } - for i := len(BPMConfig.Repositories) - 1; i >= 0; i-- { - if BPMConfig.Repositories[i].Disabled != nil && *BPMConfig.Repositories[i].Disabled { - BPMConfig.Repositories = append(BPMConfig.Repositories[:i], BPMConfig.Repositories[i+1:]...) - } - } - for _, repo := range BPMConfig.Repositories { - repo.Entries = make(map[string]*RepositoryEntry) - err := repo.ReadLocalDatabase() - if err != nil { - log.Fatal(err) - } - } -} diff --git a/utils/extract_utils.go b/utils/extract_utils.go deleted file mode 100644 index 6c51928..0000000 --- a/utils/extract_utils.go +++ /dev/null @@ -1,43 +0,0 @@ -package utils - -import ( - "archive/tar" - "errors" - "io" - "os" -) - -type TarballFileReader struct { - tarReader *tar.Reader - file *os.File -} - -func ReadTarballContent(tarballPath, fileToExtract string) (*TarballFileReader, error) { - file, err := os.Open(tarballPath) - if err != nil { - return nil, err - } - - tr := tar.NewReader(file) - for { - header, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - return nil, err - } - if header.Name == fileToExtract { - if header.Typeflag != tar.TypeReg { - return nil, errors.New("file to extract must be a regular file") - } - - return &TarballFileReader{ - tarReader: tr, - file: file, - }, nil - } - } - - return nil, errors.New("could not file in tarball") -} diff --git a/utils/operations.go b/utils/operations.go deleted file mode 100644 index 57772d3..0000000 --- a/utils/operations.go +++ /dev/null @@ -1,289 +0,0 @@ -package utils - -import ( - "errors" - "fmt" - "log" - "os" - "slices" - "strings" -) - -type BPMOperation struct { - Actions []OperationAction - UnresolvedDepends []string - RootDir string -} - -func (operation *BPMOperation) ActionsContainPackage(pkg string) bool { - for _, action := range operation.Actions { - if action.GetActionType() == "install" { - if action.(*InstallPackageAction).BpmPackage.PkgInfo.Name == pkg { - return true - } - } else if action.GetActionType() == "fetch" { - if action.(*FetchPackageAction).RepositoryEntry.Info.Name == pkg { - return true - } - } else if action.GetActionType() == "remove" { - if action.(*RemovePackageAction).BpmPackage.PkgInfo.Name == pkg { - return true - } - } - } - return false -} - -func (operation *BPMOperation) InsertActionAt(index int, action OperationAction) { - if len(operation.Actions) == index { // nil or empty slice or after last element - operation.Actions = append(operation.Actions, action) - } - operation.Actions = append(operation.Actions[:index+1], operation.Actions[index:]...) // index < len(a) - operation.Actions[index] = action -} - -func (operation *BPMOperation) GetTotalDownloadSize() uint64 { - var ret uint64 = 0 - for _, action := range operation.Actions { - if action.GetActionType() == "fetch" { - ret += action.(*FetchPackageAction).RepositoryEntry.DownloadSize - } - } - return ret -} - -func (operation *BPMOperation) GetTotalInstalledSize() uint64 { - var ret uint64 = 0 - for _, action := range operation.Actions { - if action.GetActionType() == "install" { - ret += action.(*InstallPackageAction).BpmPackage.GetInstalledSize() - } else if action.GetActionType() == "fetch" { - ret += action.(*FetchPackageAction).RepositoryEntry.InstalledSize - } - } - return ret -} - -func (operation *BPMOperation) GetFinalActionSize(rootDir string) int64 { - var ret int64 = 0 - for _, action := range operation.Actions { - if action.GetActionType() == "install" { - ret += int64(action.(*InstallPackageAction).BpmPackage.GetInstalledSize()) - if IsPackageInstalled(action.(*InstallPackageAction).BpmPackage.PkgInfo.Name, rootDir) { - ret -= int64(GetPackage(action.(*InstallPackageAction).BpmPackage.PkgInfo.Name, rootDir).GetInstalledSize()) - } - } else if action.GetActionType() == "fetch" { - ret += int64(action.(*FetchPackageAction).RepositoryEntry.InstalledSize) - } else if action.GetActionType() == "remove" { - ret -= int64(action.(*RemovePackageAction).BpmPackage.GetInstalledSize()) - } - } - return ret -} - -func (operation *BPMOperation) ResolveDependencies(reinstallDependencies, installOptionalDependencies, verbose bool) error { - pos := 0 - for _, value := range slices.Clone(operation.Actions) { - var pkgInfo *PackageInfo - if value.GetActionType() == "install" { - action := value.(*InstallPackageAction) - pkgInfo = action.BpmPackage.PkgInfo - } else if value.GetActionType() == "fetch" { - action := value.(*FetchPackageAction) - pkgInfo = action.RepositoryEntry.Info - } else { - pos++ - continue - } - - resolved, unresolved := pkgInfo.ResolveDependencies(&[]string{}, &[]string{}, pkgInfo.Type == "source", installOptionalDependencies, !reinstallDependencies, verbose, operation.RootDir) - - operation.UnresolvedDepends = append(operation.UnresolvedDepends, unresolved...) - - for _, depend := range resolved { - if !operation.ActionsContainPackage(depend) && depend != pkgInfo.Name { - if !reinstallDependencies && IsPackageInstalled(depend, operation.RootDir) { - continue - } - entry, _, err := GetRepositoryEntry(depend) - if err != nil { - return errors.New("could not get repository entry for package (" + depend + ")") - } - operation.InsertActionAt(pos, &FetchPackageAction{ - IsDependency: true, - RepositoryEntry: entry, - }) - pos++ - } - } - pos++ - } - - return nil -} - -func (operation *BPMOperation) ShowOperationSummary() { - if len(operation.Actions) == 0 { - fmt.Println("All packages are up to date!") - os.Exit(0) - } - - for _, value := range operation.Actions { - var pkgInfo *PackageInfo - if value.GetActionType() == "install" { - pkgInfo = value.(*InstallPackageAction).BpmPackage.PkgInfo - } else if value.GetActionType() == "fetch" { - pkgInfo = value.(*FetchPackageAction).RepositoryEntry.Info - } else { - pkgInfo = value.(*RemovePackageAction).BpmPackage.PkgInfo - fmt.Printf("%s: %s (Remove)\n", pkgInfo.Name, pkgInfo.GetFullVersion()) - continue - } - - installedInfo := GetPackageInfo(pkgInfo.Name, operation.RootDir) - sourceInfo := "" - if pkgInfo.Type == "source" { - if operation.RootDir != "/" { - log.Fatalf("cannot compile and install source packages to a different root directory") - } - sourceInfo = "(From Source)" - } - - if installedInfo == nil { - fmt.Printf("%s: %s (Install) %s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), sourceInfo) - } else { - comparison := ComparePackageVersions(*pkgInfo, *installedInfo) - if comparison < 0 { - fmt.Printf("%s: %s -> %s (Downgrade) %s\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), sourceInfo) - } else if comparison > 0 { - fmt.Printf("%s: %s -> %s (Upgrade) %s\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), sourceInfo) - } else { - fmt.Printf("%s: %s (Reinstall) %s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), sourceInfo) - } - } - } - - if operation.RootDir != "/" { - fmt.Println("Warning: Operating in " + operation.RootDir) - } - if operation.GetTotalDownloadSize() > 0 { - fmt.Printf("%s will be downloaded to complete this operation\n", UnsignedBytesToHumanReadable(operation.GetTotalDownloadSize())) - } - if operation.GetFinalActionSize(operation.RootDir) > 0 { - fmt.Printf("A total of %s will be installed after the operation finishes\n", BytesToHumanReadable(operation.GetFinalActionSize(operation.RootDir))) - } else if operation.GetFinalActionSize(operation.RootDir) < 0 { - fmt.Printf("A total of %s will be freed after the operation finishes\n", strings.TrimPrefix(BytesToHumanReadable(operation.GetFinalActionSize(operation.RootDir)), "-")) - } -} - -func (operation *BPMOperation) Execute(verbose, force bool) error { - // Fetch packages from repositories - if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool { - return action.GetActionType() == "fetch" - }) { - fmt.Println("Fetching packages from available repositories...") - for i, action := range operation.Actions { - if action.GetActionType() != "fetch" { - continue - } - entry := action.(*FetchPackageAction).RepositoryEntry - fetchedPackage, err := entry.Repository.FetchPackage(entry.Info.Name) - if err != nil { - return errors.New(fmt.Sprintf("could not fetch package (%s): %s\n", entry.Info.Name, err)) - } - bpmpkg, err := ReadPackage(fetchedPackage) - if err != nil { - return errors.New(fmt.Sprintf("could not fetch package (%s): %s\n", entry.Info.Name, err)) - } - fmt.Printf("Package (%s) was successfully fetched!\n", bpmpkg.PkgInfo.Name) - operation.Actions[i] = &InstallPackageAction{ - File: fetchedPackage, - IsDependency: action.(*FetchPackageAction).IsDependency, - BpmPackage: bpmpkg, - } - } - } - - // Determine words to be used for the following message - words := make([]string, 0) - if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool { - return action.GetActionType() == "install" - }) { - words = append(words, "Installing") - } - - if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool { - return action.GetActionType() == "remove" - }) { - words = append(words, "Removing") - } - - if len(words) == 0 { - return nil - } - fmt.Printf("%s packages...\n", strings.Join(words, "/")) - - // Installing/Removing packages from system - for _, action := range operation.Actions { - if action.GetActionType() == "remove" { - pkgInfo := action.(*RemovePackageAction).BpmPackage.PkgInfo - err := RemovePackage(pkgInfo.Name, verbose, operation.RootDir) - if err != nil { - return errors.New(fmt.Sprintf("could not remove package (%s): %s\n", pkgInfo.Name, err)) - } - } else if action.GetActionType() == "install" { - value := action.(*InstallPackageAction) - bpmpkg := value.BpmPackage - var err error - if value.IsDependency { - err = InstallPackage(value.File, operation.RootDir, verbose, true, false, false, false) - } else { - err = InstallPackage(value.File, operation.RootDir, verbose, force, false, false, false) - } - if err != nil { - return errors.New(fmt.Sprintf("could not install package (%s): %s\n", bpmpkg.PkgInfo.Name, err)) - } - fmt.Printf("Package (%s) was successfully installed\n", bpmpkg.PkgInfo.Name) - if value.IsDependency { - err := SetInstallationReason(bpmpkg.PkgInfo.Name, Dependency, operation.RootDir) - if err != nil { - return errors.New(fmt.Sprintf("could not set installation reason for package (%s): %s\n", value.BpmPackage.PkgInfo.Name, err)) - } - } - } - } - fmt.Println("Operation complete!") - - return nil -} - -type OperationAction interface { - GetActionType() string -} - -type InstallPackageAction struct { - File string - IsDependency bool - BpmPackage *BPMPackage -} - -func (action *InstallPackageAction) GetActionType() string { - return "install" -} - -type FetchPackageAction struct { - IsDependency bool - RepositoryEntry *RepositoryEntry -} - -func (action *FetchPackageAction) GetActionType() string { - return "fetch" -} - -type RemovePackageAction struct { - BpmPackage *BPMPackage -} - -func (action *RemovePackageAction) GetActionType() string { - return "remove" -} diff --git a/utils/package_utils.go b/utils/package_utils.go deleted file mode 100644 index 1ef7f16..0000000 --- a/utils/package_utils.go +++ /dev/null @@ -1,1635 +0,0 @@ -package utils - -import ( - "archive/tar" - "compress/gzip" - "errors" - "fmt" - version "github.com/knqyf263/go-rpm-version" - "gopkg.in/yaml.v3" - "io" - "io/fs" - "os" - "os/exec" - "path" - "path/filepath" - "slices" - "sort" - "strconv" - "strings" - "syscall" -) - -type BPMPackage struct { - PkgInfo *PackageInfo - PkgFiles []*PackageFileEntry -} - -type PackageInfo struct { - Name string `yaml:"name,omitempty"` - 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"` - Type string `yaml:"type,omitempty"` - Keep []string `yaml:"keep,omitempty"` - Depends []string `yaml:"depends,omitempty"` - MakeDepends []string `yaml:"make_depends,omitempty"` - OptionalDepends []string `yaml:"optional_depends,omitempty"` - Conflicts []string `yaml:"conflicts,omitempty"` - Provides []string `yaml:"provides,omitempty"` -} - -type PackageFileEntry struct { - Path string - OctalPerms uint32 - UserID int - GroupID int - SizeInBytes uint64 -} - -func (pkg *BPMPackage) GetInstalledSize() uint64 { - var totalSize uint64 = 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) -} - -type InstallationReason string - -const ( - Manual InstallationReason = "manual" - Dependency InstallationReason = "dependency" - Unknown InstallationReason = "unknown" -) - -func ComparePackageVersions(info1, info2 PackageInfo) int { - v1 := version.NewVersion(info1.GetFullVersion()) - v2 := version.NewVersion(info2.GetFullVersion()) - - return v1.Compare(v2) -} - -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 Manual - } - b, err := os.ReadFile(path.Join(pkgDir, "installation_reason")) - if err != nil { - return Unknown - } - reason := string(b) - if reason == "manual" { - return Manual - } else if reason == "dependency" { - return Dependency - } - return Unknown -} - -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) { - fmt.Println("a") - return nil, err - } - - file, err := os.Open(filename) - defer file.Close() - if err != nil { - return nil, err - } - - 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.ParseUint(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 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" { - bs, err := io.ReadAll(tr) - if err != nil { - return nil, err - } - ret[header.Name] = string(bs) - } else if header.Name == "post_install.sh" { - bs, err := io.ReadAll(tr) - if err != nil { - return nil, err - } - ret[header.Name] = string(bs) - } else if header.Name == "pre_update.sh" { - bs, err := io.ReadAll(tr) - if err != nil { - return nil, err - } - ret[header.Name] = string(bs) - } else if header.Name == "post_update.sh" { - bs, err := io.ReadAll(tr) - if err != nil { - return nil, err - } - ret[header.Name] = string(bs) - } else if 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 -} - -type Operation uint8 - -const ( - Install Operation = 0 - Update = 1 - Remove = 2 -) - -func ExecutePackageScripts(filename, rootDir string, operation Operation, postOperation bool) error { - pkgInfo, err := ReadPackage(filename) - if err != nil { - return err - } - scripts, err := ReadPackageScripts(filename) - if err != nil { - return err - } - - run := func(name, content string) error { - temp, err := os.CreateTemp("", name) - if err != nil { - return err - } - _, err = temp.WriteString(content) - if err != nil { - return err - } - - cmd := exec.Command("/bin/bash", temp.Name()) - if !BPMConfig.SilentCompilation { - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - } - cmd.Dir = rootDir - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_ROOT=%s", rootDir)) - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_NAME=%s", pkgInfo.PkgInfo.Name)) - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_DESC=%s", pkgInfo.PkgInfo.Description)) - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_VERSION=%s", pkgInfo.PkgInfo.Version)) - if operation != Install { - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_OLD_VERSION=%s", GetPackageInfo(pkgInfo.PkgInfo.Name, rootDir).Version)) - } - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_REVISION=%d", pkgInfo.PkgInfo.Revision)) - if operation != Install { - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_OLD_REVISION=%d", GetPackageInfo(pkgInfo.PkgInfo.Name, rootDir).Revision)) - } - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_URL=%s", pkgInfo.PkgInfo.Url)) - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_ARCH=%s", pkgInfo.PkgInfo.Arch)) - depends := make([]string, len(pkgInfo.PkgInfo.Depends)) - copy(depends, pkgInfo.PkgInfo.Depends) - for i := 0; i < len(depends); i++ { - depends[i] = fmt.Sprintf("\"%s\"", depends[i]) - } - makeDepends := make([]string, len(pkgInfo.PkgInfo.MakeDepends)) - copy(makeDepends, pkgInfo.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") - err = cmd.Run() - if err != nil { - return err - } - return nil - } - - if operation == Install { - if val, ok := scripts["pre_install.sh"]; !postOperation && ok { - err := run("pre_install.sh", val) - if err != nil { - return err - } - } - if val, ok := scripts["post_install.sh"]; postOperation && ok { - err := run("post_install.sh", val) - if err != nil { - return err - } - } - } else if operation == Update { - if val, ok := scripts["pre_update.sh"]; !postOperation && ok { - err := run("pre_update.sh", val) - if err != nil { - return err - } - } - if val, ok := scripts["post_update.sh"]; postOperation && ok { - err := run("post_update.sh", val) - if err != nil { - return err - } - } - } else if operation == Remove { - if val, ok := scripts["pre_remove.sh"]; !postOperation && ok { - err := run("pre_remove.sh", val) - if err != nil { - return err - } - } - if val, ok := scripts["post_remove.sh"]; postOperation && ok { - err := run("post_remove.sh", val) - if err != nil { - return err - } - } - } - return nil -} - -func ReadPackageInfo(contents string) (*PackageInfo, error) { - pkgInfo := PackageInfo{ - Name: "", - Description: "", - Version: "", - Revision: 1, - Url: "", - License: "", - Arch: "", - Type: "", - Keep: make([]string, 0), - Depends: make([]string, 0), - MakeDepends: make([]string, 0), - OptionalDepends: make([]string, 0), - Conflicts: make([]string, 0), - Provides: make([]string, 0), - } - err := yaml.Unmarshal([]byte(contents), &pkgInfo) - if err != nil { - return nil, err - } - 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 i := 0; i < len(pkgInfo.Keep); i++ { - pkgInfo.Keep[i] = strings.TrimPrefix(pkgInfo.Keep[i], "/") - } - return &pkgInfo, nil -} - -func CreateInfoFile(pkgInfo *PackageInfo) string { - b, err := yaml.Marshal(&pkgInfo) - if err != nil { - return "" - } - return string(b) -} - -func CreateReadableInfo(showArchitecture, showType, showPackageRelations bool, pkgInfo *PackageInfo, 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()) - ret = append(ret, "URL: "+pkgInfo.Url) - ret = append(ret, "License: "+pkgInfo.License) - if showArchitecture { - ret = append(ret, "Architecture: "+pkgInfo.Arch) - } - if showType { - ret = append(ret, "Type: "+pkgInfo.Type) - } - if showPackageRelations { - appendArray("Dependencies", pkgInfo.Depends) - appendArray("Make Dependencies", pkgInfo.MakeDepends) - appendArray("Optional dependencies", pkgInfo.OptionalDepends) - appendArray("Conflicting packages", pkgInfo.Conflicts) - appendArray("Provided packages", pkgInfo.Provides) - - } - ret = append(ret, "Installation Reason: "+string(GetInstallationReason(pkgInfo.Name, rootDir))) - return strings.Join(ret, "\n") -} - -func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string) error { - if !IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) { - err := ExecutePackageScripts(filename, rootDir, Install, false) - if err != nil { - return err - } - } else { - err := ExecutePackageScripts(filename, rootDir, Update, false) - if err != nil { - return err - } - } - seenHardlinks := make(map[string]string) - file, err := os.Open(filename) - if err != nil { - return err - } - - tarballFile, err := ReadTarballContent(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.Mkdir(extractFilename, 0755); err != nil { - if !os.IsExist(err) { - return err - } - } else { - if verbose { - fmt.Println("Creating Directory: " + extractFilename) - } - } - 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.Println("Skipping File: " + extractFilename + " (Containing directory is set to be kept during reinstalls/updates)") - } - skip = true - continue - } - } else { - if header.Name == k { - if verbose { - fmt.Println("Skipping File: " + extractFilename + " (File is configured to be kept during reinstalls/updates)") - } - skip = true - continue - } - } - } - } - if skip { - continue - } - err := os.Remove(extractFilename) - if err != nil && !os.IsNotExist(err) { - return err - } - outFile, err := os.Create(extractFilename) - if verbose { - fmt.Println("Creating File: " + extractFilename) - } - if err != nil { - return err - } - if _, err := io.Copy(outFile, packageFilesReader); err != nil { - return err - } - if err := os.Chmod(extractFilename, header.FileInfo().Mode()); err != nil { - return err - } - err = outFile.Close() - if err != nil { - return err - } - case tar.TypeSymlink: - if verbose { - fmt.Println("Creating Symlink: " + extractFilename + " -> " + header.Linkname) - } - err := os.Remove(extractFilename) - if err != nil && !os.IsNotExist(err) { - return err - } - err = os.Symlink(header.Linkname, extractFilename) - if err != nil { - return err - } - 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 - } - default: - return errors.New("unknown type (" + strconv.Itoa(int(header.Typeflag)) + ") in " + extractFilename) - } - } - for extractFilename, destination := range seenHardlinks { - if verbose { - fmt.Println("Creating Hard Link: " + extractFilename + " -> " + path.Join(rootDir, destination)) - } - err := os.Link(path.Join(rootDir, destination), extractFilename) - if err != nil { - return err - } - } - defer archive.Close() - defer file.Close() - return nil -} - -func isSplitPackage(filename string) bool { - pkgInfo, err := ReadPackage(filename) - if err != nil { - return false - } - if pkgInfo.PkgInfo.Type != "source" { - return false - } - cmd := exec.Command("/bin/bash", "-c", fmt.Sprintf("test $(tar -tf %s | grep '^pkg.info' | wc -l) -eq 1", filename)) - if err := cmd.Run(); err == nil { - return false - } - return true -} - -func compilePackage(bpmpkg *BPMPackage, filename, rootDir string, verbose, binaryPkgFromSrc, skipCheck, keepTempDir bool) (error, []string) { - var files []string - if !IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) { - err := ExecutePackageScripts(filename, rootDir, Install, false) - if err != nil { - return err, nil - } - } else { - err := ExecutePackageScripts(filename, rootDir, Update, false) - if err != nil { - return err, nil - } - } - //seenHardlinks := make(map[string]string) - file, err := os.Open(filename) - if err != nil { - return err, nil - } - - tr := tar.NewReader(file) - - temp := path.Join(BPMConfig.CompilationDir, "bpm_source-"+bpmpkg.PkgInfo.Name) - err = os.RemoveAll(temp) - if err != nil { - return err, nil - } - if verbose { - fmt.Println("Creating temp directory at: " + temp) - } - err = os.Mkdir(temp, 0755) - if err != nil { - return err, nil - } - err = os.Chown(temp, 65534, 65534) - if err != nil { - return err, nil - } - - for { - header, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - return err, nil - } - if strings.HasPrefix(header.Name, "source-files/") && header.Name != "source-files/" { - extractFilename := path.Join(temp, strings.TrimPrefix(header.Name, "source-files/")) - switch header.Typeflag { - case tar.TypeDir: - if err := os.Mkdir(extractFilename, 0755); err != nil { - if !os.IsExist(err) { - return err, nil - } - } else { - if verbose { - fmt.Println("Creating Directory: " + extractFilename) - } - err = os.Chown(extractFilename, 65534, 65534) - if err != nil { - return err, nil - } - } - case tar.TypeReg: - err := os.Remove(extractFilename) - if err != nil && !os.IsNotExist(err) { - return err, nil - } - outFile, err := os.Create(extractFilename) - if verbose { - fmt.Println("Creating File: " + extractFilename) - } - if err != nil { - return err, nil - } - err = os.Chown(extractFilename, 65534, 65534) - if err != nil { - return err, nil - } - if _, err := io.Copy(outFile, tr); err != nil { - return err, nil - } - if err := os.Chmod(extractFilename, header.FileInfo().Mode()); err != nil { - return err, nil - } - err = outFile.Close() - if err != nil { - return err, nil - } - case tar.TypeSymlink: - if verbose { - fmt.Println("Skipping symlink (Bundling symlinks in source packages is not supported)") - } - case tar.TypeLink: - if verbose { - fmt.Println("Skipping hard link (Bundling hard links in source packages is not supported)") - } - default: - return errors.New("unknown type (" + strconv.Itoa(int(header.Typeflag)) + ") in " + extractFilename), nil - } - } - if header.Name == "source.sh" { - bs, err := io.ReadAll(tr) - if err != nil { - return err, nil - } - err = os.WriteFile(path.Join(temp, "source.sh"), bs, 0644) - if err != nil { - return err, nil - } - err = os.Chown(path.Join(temp, "source.sh"), 65534, 65534) - if err != nil { - return err, nil - } - } - } - if _, err := os.Stat(path.Join(temp, "source.sh")); os.IsNotExist(err) { - return errors.New("source.sh file could not be found in the temporary build directory"), nil - } - fmt.Println("Running source.sh file...") - if !IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) { - err = ExecutePackageScripts(filename, rootDir, Install, false) - if err != nil { - return err, nil - } - } else { - err = ExecutePackageScripts(filename, rootDir, Update, false) - if err != nil { - return err, nil - } - } - bs, err := os.ReadFile(path.Join(temp, "source.sh")) - if err != nil { - return err, nil - } - - if !strings.Contains(string(bs), "package()") { - fmt.Print("This package does not seem to have the required 'package' function\nThe source.sh file may have been created for an older BPM version\nPlease update the source.sh file") - return errors.New("invalid source.sh format"), nil - } - - runScript := ` -cd "$BPM_WORKDIR" - -set -a -source "source.sh" -set +a - -if [[ $(type -t prepare) == function ]]; then - echo "Running prepare() function..." - bash -e -c prepare - if [ $? -ne 0 ]; then - echo "Failed to run prepare() function in source.sh" - exit 1 - fi -fi - -cd "$BPM_SOURCE" -if [[ $(type -t build) == function ]]; then - echo "Running build() function..." - bash -e -c build - if [ $? -ne 0 ]; then - echo "Failed to run build() function in source.sh" - exit 1 - fi -fi - -cd "$BPM_SOURCE" -if [[ $(type -t check) == function ]] && [ -z "$SKIPCHECK" ]; then - echo "Running check() function..." - bash -e -c check - if [ $? -ne 0 ]; then - echo "Failed to run check() function in source.sh" - exit 1 - fi -fi - - -cd "$BPM_SOURCE" -if ! [[ $(type -t package) == function ]]; then - echo "Failed to locate package() function in source.sh" - exit 1 -fi -echo "Running package() function..." -touch "$BPM_WORKDIR"/fakeroot_file -fakeroot -s "$BPM_WORKDIR"/fakeroot_file bash -e -c package -bash -e -c package -if [ $? -ne 0 ]; then - echo "Failed to run package() function in source.sh" -fi -` - err = os.WriteFile(path.Join(temp, "run.sh"), []byte(runScript), 0644) - if err != nil { - return err, nil - } - err = os.Chown(path.Join(temp, "run.sh"), 65534, 65534) - if err != nil { - return err, nil - } - - cmd := exec.Command("/bin/bash", "-e", "run.sh") - cmd.SysProcAttr = &syscall.SysProcAttr{} - cmd.SysProcAttr.Credential = &syscall.Credential{Uid: 65534, Gid: 65534} - cmd.Dir = temp - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, "USER=nobody") - cmd.Env = append(cmd.Env, "HOME="+temp) - - err = os.Mkdir(path.Join(temp, "source"), 0755) - if err != nil { - return err, nil - } - err = os.Chown(path.Join(temp, "source"), 65534, 65534) - if err != nil { - return err, nil - } - err = os.Mkdir(path.Join(temp, "output"), 0755) - if err != nil { - return err, nil - } - err = os.Chown(path.Join(temp, "output"), 65534, 65534) - if err != nil { - return err, nil - } - cmd.Env = append(cmd.Env, "BPM_WORKDIR="+temp) - cmd.Env = append(cmd.Env, "BPM_SOURCE="+path.Join(temp, "source")) - cmd.Env = append(cmd.Env, "BPM_OUTPUT="+path.Join(temp, "output")) - cmd.Env = append(cmd.Env, "SKIPCHECK="+strconv.FormatBool(skipCheck)) - - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_ROOT=%s", rootDir)) - 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)) - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_REVISION=%d", bpmpkg.PkgInfo.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, " "))) - for _, value := range BPMConfig.CompilationEnv { - cmd.Env = append(cmd.Env, value) - } - cmd.Env = append(cmd.Env, "BPM_PKG_TYPE=source") - - if !BPMConfig.SilentCompilation { - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - } - err = cmd.Run() - if err != nil { - return err, nil - } - if _, err := os.Stat(path.Join(temp, "output/")); err != nil { - if os.IsNotExist(err) { - return errors.New("output directory not be found at " + path.Join(temp, "output/")), nil - } - return err, nil - } - if dir, _ := os.ReadDir(path.Join(temp, "output/")); len(dir) == 0 { - return errors.New("output directory is empty"), nil - } - fmt.Println("Copying all files...") - err = filepath.WalkDir(path.Join(temp, "/output/"), func(fullpath string, d fs.DirEntry, err error) error { - relFilename, err := filepath.Rel(path.Join(temp, "/output/"), fullpath) - if relFilename == "." { - return nil - } - extractFilename := path.Join(rootDir, relFilename) - if err != nil { - return err - } - if d.Type() == os.ModeDir { - files = append(files, relFilename+"/") - if err := os.Mkdir(extractFilename, 0755); err != nil { - if !os.IsExist(err) { - return err - } - } else { - if verbose { - fmt.Println("Creating Directory: " + extractFilename) - } - } - } else if d.Type().IsRegular() { - if _, err := os.Stat(extractFilename); err == nil { - if slices.Contains(bpmpkg.PkgInfo.Keep, relFilename) { - if verbose { - fmt.Println("Skipping File: " + extractFilename + "(File is configured to be kept during installs/updates)") - } - files = append(files, relFilename) - return nil - } - } - err := os.Remove(extractFilename) - if err != nil && !os.IsNotExist(err) { - return err - } - outFile, err := os.Create(extractFilename) - if verbose { - fmt.Println("Creating File: " + extractFilename) - } - files = append(files, relFilename) - if err != nil { - return err - } - f, err := os.Open(fullpath) - if err != nil { - return err - } - if _, err := io.Copy(outFile, f); err != nil { - return err - } - info, err := os.Stat(fullpath) - if err != nil { - return err - } - if err := os.Chmod(extractFilename, info.Mode()); err != nil { - return err - } - err = outFile.Close() - if err != nil { - return err - } - err = f.Close() - if err != nil { - return err - } - } else if d.Type() == os.ModeSymlink { - link, err := os.Readlink(fullpath) - if err != nil { - return err - } - err = os.Remove(extractFilename) - if err != nil && !os.IsNotExist(err) { - return err - } - if verbose { - fmt.Println("Creating Symlink: "+extractFilename, " -> "+link) - } - files = append(files, relFilename) - err = os.Symlink(link, extractFilename) - if err != nil { - return err - } - } - return nil - }) - if err != nil { - return err, nil - } - if binaryPkgFromSrc { - compiledDir := path.Join(BPMConfig.BinaryOutputDir) - err = os.MkdirAll(compiledDir, 0755) - compiledInfo := PackageInfo{} - compiledInfo = *bpmpkg.PkgInfo - compiledInfo.Type = "binary" - compiledInfo.Arch = GetArch() - err = os.WriteFile(path.Join(temp, "pkg.info"), []byte(CreateInfoFile(&compiledInfo)), 0644) - if err != nil { - return err, nil - } - err = os.Chown(path.Join(temp, "pkg.info"), 65534, 65534) - if err != nil { - return err, nil - } - scripts, err := ReadPackageScripts(filename) - for key, val := range scripts { - err = os.WriteFile(path.Join(temp, key), []byte(val), 0644) - if err != nil { - return err, nil - } - err = os.Chown(path.Join(temp, key), 65534, 65534) - if err != nil { - return err, nil - } - } - sed := fmt.Sprintf("s/output/files/") - fileName := compiledInfo.Name + "-" + compiledInfo.GetFullVersion() + "-" + compiledInfo.Arch + ".bpm" - cmd := exec.Command("/usr/bin/fakeroot", "-i fakeroot_file", "tar", "-czvpf", fileName, "pkg.info", "output/", "--transform", sed) - if !BPMConfig.SilentCompilation { - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - } - cmd.SysProcAttr = &syscall.SysProcAttr{} - cmd.SysProcAttr.Credential = &syscall.Credential{Uid: 65534, Gid: 65534} - cmd.Dir = temp - cmd.Env = os.Environ() - fmt.Printf("running command: %s\n", strings.Join(cmd.Args, " ")) - err = cmd.Run() - if err != nil { - return err, nil - } - err = copyFileContents(path.Join(temp, fileName), path.Join(compiledDir, fileName)) - if err != nil { - return err, nil - } - err = os.Chown(path.Join(compiledDir, fileName), 0, 0) - if err != nil { - return err, nil - } - } - if !keepTempDir { - err := os.RemoveAll(temp) - if err != nil { - return err, nil - } - } - if len(files) == 0 { - return errors.New("no output files for source package. Cancelling package installation"), nil - } - - defer file.Close() - return nil, files -} - -func InstallPackage(filename, rootDir string, verbose, force, binaryPkgFromSrc, skipCheck, keepTempDir bool) error { - if _, err := os.Stat(filename); os.IsNotExist(err) { - return err - } - bpmpkg, err := ReadPackage(filename) - if err != nil { - return err - } - packageInstalled := IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) - // Check if package is installed and remove current files - if packageInstalled { - // Fetching and reversing package file entry list - fileEntries := GetPackageFiles(bpmpkg.PkgInfo.Name, rootDir) - 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) - } - - if bpmpkg.PkgInfo.Type == "binary" { - err := extractPackage(bpmpkg, verbose, filename, rootDir) - if err != nil { - return err - } - } else if bpmpkg.PkgInfo.Type == "source" { - if isSplitPackage(filename) { - return errors.New("BPM is unable to install split source packages") - } - err, _ := compilePackage(bpmpkg, filename, rootDir, verbose, binaryPkgFromSrc, skipCheck, keepTempDir) - if err != nil { - return err - } - } else { - return errors.New("unknown package type: " + bpmpkg.PkgInfo.Type) - } - - 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.RemoveAll(pkgDir) - if err != nil { - return err - } - err = os.Mkdir(pkgDir, 0755) - if err != nil { - return err - } - - f, err := os.Create(path.Join(pkgDir, "files")) - if err != nil { - return err - } - - tarballFile, err := ReadTarballContent(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 - } - - scripts, err := ReadPackageScripts(filename) - if err != nil { - return err - } - if val, ok := scripts["post_remove.sh"]; ok { - f, err = os.Create(path.Join(pkgDir, "post_remove.sh")) - if err != nil { - return err - } - _, err = f.WriteString(val) - if err != nil { - return err - } - err = f.Close() - if err != nil { - return err - } - } - - if !packageInstalled { - err = ExecutePackageScripts(filename, rootDir, Install, true) - if err != nil { - return err - } - } else { - err = ExecutePackageScripts(filename, rootDir, Update, true) - if err != nil { - return err - } - } - return nil -} - -func (pkgInfo *PackageInfo) GetAllDependencies(checkMake, checkOptional bool) []string { - allDepends := make([]string, 0) - allDepends = append(allDepends, pkgInfo.Depends...) - if checkMake { - allDepends = append(allDepends, pkgInfo.MakeDepends...) - } - if checkOptional { - allDepends = append(allDepends, pkgInfo.OptionalDepends...) - } - return allDepends -} - -func (pkgInfo *PackageInfo) CheckDependencies(checkMake, checkOptional bool, rootDir string) []string { - var ret []string - for _, dependency := range pkgInfo.Depends { - if !IsPackageProvided(dependency, rootDir) { - ret = append(ret, dependency) - } - } - if checkMake { - for _, dependency := range pkgInfo.MakeDepends { - if !IsPackageProvided(dependency, rootDir) { - ret = append(ret, dependency) - } - } - } - if checkOptional { - for _, dependency := range pkgInfo.OptionalDepends { - if !IsPackageProvided(dependency, rootDir) { - ret = append(ret, dependency) - } - } - } - - return ret -} - -func (pkgInfo *PackageInfo) CheckConflicts(rootDir string) []string { - var ret []string - for _, conflict := range pkgInfo.Conflicts { - if IsPackageInstalled(conflict, rootDir) { - ret = append(ret, conflict) - } - } - return ret -} - -func (pkgInfo *PackageInfo) ResolveDependencies(resolved, unresolved *[]string, checkMake, checkOptional, ignoreInstalled, verbose bool, rootDir string) ([]string, []string) { - *unresolved = append(*unresolved, pkgInfo.Name) - for _, depend := range pkgInfo.GetAllDependencies(checkMake, checkOptional) { - depend = strings.TrimSpace(depend) - depend = strings.ToLower(depend) - if !slices.Contains(*resolved, depend) { - if slices.Contains(*unresolved, depend) { - if verbose { - fmt.Printf("Circular dependency was detected (%s -> %s). Installing %s first\n", pkgInfo.Name, depend, depend) - } - if !slices.Contains(*resolved, depend) { - *resolved = append(*resolved, depend) - } - continue - } else if ignoreInstalled && IsPackageInstalled(depend, rootDir) { - continue - } - entry, _, err := GetRepositoryEntry(depend) - if err != nil { - if !slices.Contains(*unresolved, depend) { - *unresolved = append(*unresolved, depend) - } - continue - } - entry.Info.ResolveDependencies(resolved, unresolved, checkMake, checkOptional, ignoreInstalled, verbose, rootDir) - } - } - if !slices.Contains(*resolved, pkgInfo.Name) { - *resolved = append(*resolved, pkgInfo.Name) - } - *unresolved = stringSliceRemove(*unresolved, pkgInfo.Name) - return *resolved, *unresolved -} - -func IsPackageInstalled(pkg, rootDir string) bool { - installedDir := path.Join(rootDir, "var/lib/bpm/installed/") - pkgDir := path.Join(installedDir, pkg) - if _, err := os.Stat(pkgDir); err != nil { - return false - } - return true -} - -func IsPackageProvided(pkg, rootDir string) bool { - pkgs, err := GetInstalledPackages(rootDir) - if err != nil { - return false - } - for _, p := range pkgs { - if p == pkg { - return true - } - i := GetPackageInfo(p, rootDir) - if i == nil { - continue - } - if slices.Contains(i.Provides, pkg) { - return true - } - } - return false -} - -func GetInstalledPackages(rootDir string) ([]string, error) { - installedDir := path.Join(rootDir, "var/lib/bpm/installed/") - items, err := os.ReadDir(installedDir) - if os.IsNotExist(err) { - return nil, nil - } - if err != nil { - return nil, err - } - var ret []string - for _, item := range items { - ret = append(ret, item.Name()) - } - return ret, nil -} - -func GetPackageFiles(pkg, rootDir string) []*PackageFileEntry { - var pkgFiles []*PackageFileEntry - installedDir := path.Join(rootDir, "var/lib/bpm/installed/") - pkgDir := path.Join(installedDir, pkg) - files := path.Join(pkgDir, "files") - if _, err := os.Stat(installedDir); os.IsNotExist(err) { - return nil - } - if _, err := os.Stat(pkgDir); os.IsNotExist(err) { - return nil - } - bs, err := os.ReadFile(files) - if err != nil { - return nil - } - - for _, line := range strings.Split(string(bs), "\n") { - if strings.TrimSpace(line) == "" { - continue - } - stringEntry := strings.Split(strings.TrimSpace(line), " ") - if len(stringEntry) < 5 { - pkgFiles = append(pkgFiles, &PackageFileEntry{ - Path: strings.TrimSuffix(line, "/"), - OctalPerms: 0, - UserID: 0, - GroupID: 0, - SizeInBytes: 0, - }) - continue - } - uid, err := strconv.ParseInt(stringEntry[len(stringEntry)-4], 0, 32) - if err != nil { - return nil - } - octalPerms, err := strconv.ParseInt(stringEntry[len(stringEntry)-3], 0, 32) - if err != nil { - return nil - } - gid, err := strconv.ParseInt(stringEntry[len(stringEntry)-2], 0, 32) - if err != nil { - return nil - } - size, err := strconv.ParseUint(stringEntry[len(stringEntry)-1], 0, 64) - if err != nil { - return nil - } - pkgFiles = append(pkgFiles, &PackageFileEntry{ - Path: strings.TrimSuffix(strings.Join(stringEntry[:len(stringEntry)-4], " "), "/"), - OctalPerms: uint32(octalPerms), - UserID: int(uid), - GroupID: int(gid), - SizeInBytes: size, - }) - } - - return pkgFiles -} - -func GetPackageInfo(pkg, rootDir string) *PackageInfo { - installedDir := path.Join(rootDir, "var/lib/bpm/installed/") - pkgDir := path.Join(installedDir, pkg) - files := path.Join(pkgDir, "info") - if _, err := os.Stat(installedDir); os.IsNotExist(err) { - return nil - } - if _, err := os.Stat(pkgDir); os.IsNotExist(err) { - return nil - } - file, err := os.Open(files) - if err != nil { - return nil - } - bs, err := io.ReadAll(file) - if err != nil { - return nil - } - info, err := ReadPackageInfo(string(bs)) - if err != nil { - return nil - } - return info -} - -func GetPackage(pkg, rootDir string) *BPMPackage { - if !IsPackageInstalled(pkg, rootDir) { - return nil - } - - return &BPMPackage{ - PkgInfo: GetPackageInfo(pkg, rootDir), - PkgFiles: GetPackageFiles(pkg, rootDir), - } -} - -func GetAllPackageFiles(rootDir string, excludePackages ...string) (map[string][]*BPMPackage, error) { - ret := make(map[string][]*BPMPackage) - - pkgNames, err := GetInstalledPackages(rootDir) - if err != nil { - return nil, err - } - - for _, pkgName := range pkgNames { - if slices.Contains(excludePackages, pkgName) { - continue - } - bpmpkg := GetPackage(pkgName, rootDir) - if bpmpkg == nil { - return nil, errors.New(fmt.Sprintf("could not get BPM package (%s)", pkgName)) - } - for _, entry := range bpmpkg.PkgFiles { - if _, ok := ret[entry.Path]; ok { - ret[entry.Path] = append(ret[entry.Path], bpmpkg) - } else { - ret[entry.Path] = []*BPMPackage{bpmpkg} - } - } - } - - return ret, nil -} - -func RemovePackage(pkg string, verbose bool, rootDir string) error { - installedDir := path.Join(rootDir, "var/lib/bpm/installed/") - pkgDir := path.Join(installedDir, pkg) - pkgInfo := GetPackageInfo(pkg, rootDir) - if pkgInfo == nil { - return errors.New("could not get package info") - } - - // Fetching and reversing package file entry list - fileEntries := GetPackageFiles(pkg, rootDir) - 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 - } - - // Removing package files - for _, entry := range fileEntries { - 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 - if _, err := os.Stat(path.Join(pkgDir, "post_remove.sh")); err == nil { - cmd := exec.Command("/bin/bash", path.Join(pkgDir, "post_remove.sh")) - if !BPMConfig.SilentCompilation { - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - } - cmd.Dir = rootDir - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_ROOT=%s", rootDir)) - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_NAME=%s", pkgInfo.Name)) - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_DESC=%s", pkgInfo.Description)) - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_VERSION=%s", pkgInfo.Version)) - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_REVISION=%d", pkgInfo.Revision)) - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_URL=%s", pkgInfo.Url)) - cmd.Env = append(cmd.Env, fmt.Sprintf("BPM_PKG_ARCH=%s", pkgInfo.Arch)) - depends := make([]string, len(pkgInfo.Depends)) - copy(depends, pkgInfo.Depends) - for i := 0; i < len(depends); i++ { - depends[i] = fmt.Sprintf("\"%s\"", depends[i]) - } - makeDepends := make([]string, len(pkgInfo.MakeDepends)) - copy(makeDepends, 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") - err = cmd.Run() - if err != nil { - return err - } - } - - // Removing package directory - if verbose { - fmt.Println("Removing: " + pkgDir) - } - err = os.RemoveAll(pkgDir) - if err != nil { - return err - } - - return nil -} diff --git a/utils/repo_utils.go b/utils/repo_utils.go deleted file mode 100644 index 28fdaeb..0000000 --- a/utils/repo_utils.go +++ /dev/null @@ -1,197 +0,0 @@ -package utils - -import ( - "errors" - "gopkg.in/yaml.v3" - "io" - "net/http" - "net/url" - "os" - "path" - "sort" - "strings" -) - -type Repository struct { - Name string `yaml:"name"` - Source string `yaml:"source"` - Disabled *bool `yaml:"disabled"` - Entries map[string]*RepositoryEntry -} - -type RepositoryEntry struct { - Info *PackageInfo `yaml:"info"` - Download string `yaml:"download"` - DownloadSize uint64 `yaml:"download_size"` - InstalledSize uint64 `yaml:"installed_size"` - IsVirtualPackage bool `yaml:"-"` - Repository *Repository -} - -func (repo *Repository) ContainsPackage(pkg string) bool { - _, ok := repo.Entries[pkg] - return ok -} - -func (repo *Repository) ReadLocalDatabase() error { - repoFile := "/var/lib/bpm/repositories/" + repo.Name + ".bpmdb" - if _, err := os.Stat(repoFile); err != nil { - return nil - } - - bytes, err := os.ReadFile(repoFile) - if err != nil { - return err - } - - virtualPackages := make(map[string][]string) - - data := string(bytes) - for _, b := range strings.Split(data, "---") { - entry := RepositoryEntry{ - Info: &PackageInfo{ - Name: "", - Description: "", - Version: "", - Revision: 1, - Url: "", - License: "", - Arch: "", - Type: "", - Keep: make([]string, 0), - Depends: make([]string, 0), - MakeDepends: make([]string, 0), - OptionalDepends: make([]string, 0), - Conflicts: make([]string, 0), - Provides: make([]string, 0), - }, - Download: "", - DownloadSize: 0, - InstalledSize: 0, - IsVirtualPackage: false, - Repository: repo, - } - err := yaml.Unmarshal([]byte(b), &entry) - if err != nil { - return err - } - - for _, p := range entry.Info.Provides { - virtualPackages[p] = append(virtualPackages[p], entry.Info.Name) - } - repo.Entries[entry.Info.Name] = &entry - } - - for key, value := range virtualPackages { - if _, ok := repo.Entries[key]; ok { - continue - } - sort.Strings(value) - entry := RepositoryEntry{ - Info: repo.Entries[value[0]].Info, - Download: repo.Entries[value[0]].Download, - DownloadSize: repo.Entries[value[0]].DownloadSize, - InstalledSize: repo.Entries[value[0]].InstalledSize, - IsVirtualPackage: true, - Repository: repo, - } - repo.Entries[key] = &entry - } - return nil -} - -func (repo *Repository) SyncLocalDatabase() error { - repoFile := "/var/lib/bpm/repositories/" + repo.Name + ".bpmdb" - err := os.MkdirAll(path.Dir(repoFile), 0755) - if err != nil { - return err - } - - u, err := url.JoinPath(repo.Source, "database.bpmdb") - if err != nil { - return err - } - - resp, err := http.Get(u) - if err != nil { - return err - } - defer resp.Body.Close() - - out, err := os.Create(repoFile) - if err != nil { - return err - } - defer out.Close() - - _, err = io.Copy(out, resp.Body) - - return nil -} - -func GetRepository(name string) *Repository { - for _, repo := range BPMConfig.Repositories { - if repo.Name == name { - return repo - } - } - return nil -} - -func GetRepositoryEntry(str string) (*RepositoryEntry, *Repository, error) { - split := strings.Split(str, "/") - if len(split) == 1 { - pkgName := strings.TrimSpace(split[0]) - if pkgName == "" { - return nil, nil, errors.New("could not find repository entry for this package") - } - for _, repo := range BPMConfig.Repositories { - if repo.ContainsPackage(pkgName) { - return repo.Entries[pkgName], repo, nil - } - } - return nil, nil, errors.New("could not find repository entry for this package") - } else if len(split) == 2 { - repoName := strings.TrimSpace(split[0]) - pkgName := strings.TrimSpace(split[1]) - if repoName == "" || pkgName == "" { - return nil, nil, errors.New("could not find repository entry for this package") - } - repo := GetRepository(repoName) - if repo == nil || !repo.ContainsPackage(pkgName) { - return nil, nil, errors.New("could not find repository entry for this package") - } - return repo.Entries[pkgName], repo, nil - } else { - return nil, nil, errors.New("could not find repository entry for this package") - } -} - -func (repo *Repository) FetchPackage(pkg string) (string, error) { - if !repo.ContainsPackage(pkg) { - return "", errors.New("could not fetch package '" + pkg + "'") - } - entry := repo.Entries[pkg] - URL, err := url.JoinPath(repo.Source, entry.Download) - if err != nil { - return "", err - } - resp, err := http.Get(URL) - if err != nil { - return "", err - } - defer resp.Body.Close() - - err = os.MkdirAll("/var/cache/bpm/packages/", 0755) - if err != nil { - return "", err - } - out, err := os.Create("/var/cache/bpm/packages/" + path.Base(entry.Download)) - if err != nil { - return "", err - } - defer out.Close() - - _, err = io.Copy(out, resp.Body) - return "/var/cache/bpm/packages/" + path.Base(entry.Download), nil -}