diff --git a/Makefile b/Makefile index 1b4a678..57d22dc 100644 --- a/Makefile +++ b/Makefile @@ -4,11 +4,15 @@ BINDIR ?= $(PREFIX)/bin SYSCONFDIR := $(PREFIX)/etc # Compilers and tools -GO ?= $(shell which go) +GO ?= go + +# Build-time variables +ROOT_COMPILATION_UID ?= 65534 +ROOT_COMPILATION_GID ?= 65534 build: mkdir -p build - cd src/bpm; $(GO) build -ldflags "-w" -o ../../build/bpm git.enumerated.dev/bubble-package-manager/bpm/src/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/ # Create directories @@ -25,4 +29,4 @@ uninstall: clean: rm -r build/ -.PHONY: build \ No newline at end of file +.PHONY: build diff --git a/README.md b/README.md index 231a717..5813e5b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ BPM is a simple package manager for Linux systems ## Features - Simple to use subcommands -- Can install binary packages (and source packages in the future) +- Can install both binary and source packages ## Information BPM is still in very early development. Do not install it without knowing what you are doing. I would only recommend using it in a Virtual Machine for testing @@ -14,7 +14,6 @@ BPM is still in very early development. Do not install it without knowing what y - Download `go` from your package manager or from the go website - Download `make` from your package manager -- Download `which` from your package manager - 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 @@ -30,7 +29,7 @@ You are able to install bpm packages by typing the following: ```sh bpm install /path/to/package.bpm ``` -You can also use the package name directly if using repositories +You can also use the package name directly if using databases ```sh bpm install package_name ``` @@ -45,12 +44,12 @@ You can remove an installed package by typing the following bpm remove package_name ``` -To remove all unused dependencies try using the cleanup command +To remove all unused dependencies and clean cached files try using the cleanup command ```sh bpm cleanup ``` -If using repositories, all packages can be updated using this simple command +If using databases, all packages can be updated using this simple command ```sh bpm update ``` @@ -64,4 +63,4 @@ bpm help Package creation is simplified using the bpm-utils package which contains helper scripts for creating and archiving packages -Learn more here: https://git.enumerated.dev/bubble-package-manager/bpm-utils \ No newline at end of file +Learn more here: https://git.enumerated.dev/bubble-package-manager/bpm-utils diff --git a/config/bpm.conf b/config/bpm.conf index 3e8e4ae..d594d35 100644 --- a/config/bpm.conf +++ b/config/bpm.conf @@ -1,5 +1,8 @@ ignore_packages: [] -repositories: - - name: example-repository - source: https://my-repo.xyz/ +privilege_escalator_cmd: "sudo" +compilation_env: [] +cleanup_make_dependencies: true +databases: + - name: example-database + source: https://my-database.xyz/ disabled: true \ No newline at end of file diff --git a/src/bpm/main.go b/src/bpm/main.go index 9ff4a10..ffb0698 100644 --- a/src/bpm/main.go +++ b/src/bpm/main.go @@ -5,12 +5,15 @@ import ( "errors" "flag" "fmt" - "git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib" "log" "os" + "os/exec" + "path" "path/filepath" "slices" "strings" + + "git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib" ) /* -------------BPM | Bubble Package Manager-------------- */ @@ -32,12 +35,20 @@ var pkgListNumbers = false var pkgListNames = false var reinstall = false var reinstallAll = false -var noOptional = false +var installOptional = false var installationReason = "" var nosync = true var removeUnused = false var doCleanup = false -var showRepoInfo = 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() @@ -62,6 +73,7 @@ const ( remove cleanup file + compile ) func getCommandType() commandType { @@ -86,6 +98,8 @@ func getCommandType() commandType { return cleanup case "file": return file + case "compile": + return compile default: return help } @@ -104,7 +118,7 @@ func resolveCommand() { } // Read local databases - err := bpmlib.ReadLocalDatabases() + err := bpmlib.ReadLocalDatabaseFiles() if err != nil { log.Fatalf("Error: could not read local databases: %s", err) } @@ -112,13 +126,14 @@ func resolveCommand() { for n, pkg := range packages { var info *bpmlib.PackageInfo isFile := false - if showRepoInfo { + showInstallationReason := false + if showDatabaseInfo { var err error - var entry *bpmlib.RepositoryEntry - entry, _, err = bpmlib.GetRepositoryEntry(pkg) + 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 repository\n", pkg) + log.Fatalf("Error: could not find package (%s) in any database\n", pkg) } } info = entry.Info @@ -135,6 +150,7 @@ func resolveCommand() { } else { info = bpmlib.GetPackageInfo(pkg, rootDir) } + showInstallationReason = true } if info == nil { log.Fatalf("Error: package (%s) is not installed\n", pkg) @@ -149,11 +165,11 @@ func resolveCommand() { } fmt.Println("File: " + abs) } - fmt.Println(bpmlib.CreateReadableInfo(true, true, true, info, rootDir)) + fmt.Println(bpmlib.CreateReadableInfo(true, true, true, showInstallationReason, info, rootDir)) } case list: // Read local databases - err := bpmlib.ReadLocalDatabases() + err := bpmlib.ReadLocalDatabaseFiles() if err != nil { log.Fatalf("Error: could not read local databases: %s", err) } @@ -183,7 +199,7 @@ func resolveCommand() { if n != 0 { fmt.Println() } - fmt.Println(bpmlib.CreateReadableInfo(true, true, true, info, rootDir)) + fmt.Println(bpmlib.CreateReadableInfo(true, true, true, true, info, rootDir)) } } case search: @@ -193,7 +209,7 @@ func resolveCommand() { } // Read local databases - err := bpmlib.ReadLocalDatabases() + err := bpmlib.ReadLocalDatabaseFiles() if err != nil { log.Fatalf("Error: could not read local databases: %s", err) } @@ -201,8 +217,8 @@ func resolveCommand() { for i, term := range searchTerms { nameResults := make([]*bpmlib.PackageInfo, 0) descResults := make([]*bpmlib.PackageInfo, 0) - for _, repo := range bpmlib.BPMConfig.Repositories { - for _, entry := range repo.Entries { + for _, db := range bpmlib.BPMConfig.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) { @@ -241,6 +257,8 @@ func resolveCommand() { 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) @@ -257,13 +275,13 @@ func resolveCommand() { } // Read local databases - err := bpmlib.ReadLocalDatabases() + 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, !noOptional, force, verbose, subcommandArgs...) + 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 { @@ -315,14 +333,14 @@ func resolveCommand() { // Read local databases if no sync if nosync { - err := bpmlib.ReadLocalDatabases() + 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, !noOptional, force, verbose) + 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 { @@ -397,13 +415,13 @@ func resolveCommand() { } // Read local databases - err := bpmlib.ReadLocalDatabases() + 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, verbose, subcommandArgs...) + 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 { @@ -448,51 +466,58 @@ func resolveCommand() { log.Fatalf("Error: this subcommand needs to be run with superuser permissions") } - // Read local databases - err := bpmlib.ReadLocalDatabases() + err := bpmlib.CleanupCache(rootDir, cleanupCompilationFiles, cleanupCompiledPackages, cleanupFetchedPackages, verbose) if err != nil { - log.Fatalf("Error: could not read local databases: %s", err) + log.Fatalf("Error: could not complete cache cleanup: %s", err) } - // Create cleanup operation - operation, err := bpmlib.CleanupPackages(rootDir, 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 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) + if cleanupDependencies || cleanupMakeDependencies { + // Read local databases + err := bpmlib.ReadLocalDatabaseFiles() + if err != nil { + log.Fatalf("Error: could not read local databases: %s", err) } - } - // Execute operation - err = operation.Execute(verbose, force) - if err != nil { - log.Fatalf("Error: could not complete operation: %s\n", 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) + } - // Executing hooks - fmt.Println("Running hooks...") - err = operation.RunHooks(verbose) - if err != nil { - log.Fatalf("Error: could not run hooks: %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 @@ -528,7 +553,7 @@ func resolveCommand() { var pkgList []string for _, pkg := range pkgs { - if slices.ContainsFunc(bpmlib.GetPackageFiles(pkg, rootDir), func(entry *bpmlib.PackageFileEntry) bool { + if slices.ContainsFunc(bpmlib.GetPackage(pkg, rootDir).PkgFiles, func(entry *bpmlib.PackageFileEntry) bool { return entry.Path == absFile }) { pkgList = append(pkgList, pkg) @@ -543,6 +568,169 @@ func resolveCommand() { } } } + 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.BPMConfig.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.BPMConfig.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() } @@ -554,25 +742,24 @@ func printHelp() { 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, --repos] | shows information on an installed package") + 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(" --repos show information on package in repository") + 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 declared repositories") - fmt.Println("-> bpm install [-R, -v, -y, -f, -o, -c, -b, -k, --reinstall, --reinstall-all, --no-optional, --installation-reason] | installs the following files") + 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(" -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(" --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 repositories") + 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") @@ -589,12 +776,24 @@ func printHelp() { 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] | remove all unused dependency packages") + 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") } @@ -608,7 +807,7 @@ func resolveFlags() { // Info flags infoFlagSet := flag.NewFlagSet("Info flags", flag.ExitOnError) infoFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root") - infoFlagSet.BoolVar(&showRepoInfo, "repos", false, "Show information on package in repository") + infoFlagSet.BoolVar(&showDatabaseInfo, "databases", false, "Show information on package in configured databases") infoFlagSet.Usage = printHelp // Install flags installFlagSet := flag.NewFlagSet("Install flags", flag.ExitOnError) @@ -618,7 +817,7 @@ func resolveFlags() { 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.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 @@ -648,11 +847,35 @@ func resolveFlags() { 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 { @@ -694,12 +917,31 @@ func resolveFlags() { 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.BPMConfig.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..f8434e4 --- /dev/null +++ b/src/bpmlib/compilation.go @@ -0,0 +1,382 @@ +package bpmlib + +import ( + "errors" + "fmt" + "gopkg.in/yaml.v3" + "os" + "os/exec" + "path" + "strconv" + "strings" + "syscall" +) + +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, BPMConfig.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 index eee5d57..e5f33c8 100644 --- a/src/bpmlib/config.go +++ b/src/bpmlib/config.go @@ -6,8 +6,11 @@ import ( ) type BPMConfigStruct struct { - IgnorePackages []string `yaml:"ignore_packages"` - Repositories []*Repository `yaml:"repositories"` + IgnorePackages []string `yaml:"ignore_packages"` + PrivilegeEscalatorCmd string `yaml:"privilege_escalator_cmd"` + CompilationEnvironment []string `yaml:"compilation_env"` + CleanupMakeDependencies bool `yaml:"cleanup_make_dependencies"` + Databases []*BPMDatabase `yaml:"databases"` } var BPMConfig BPMConfigStruct @@ -22,15 +25,17 @@ func ReadConfig() (err error) { return err } - BPMConfig = BPMConfigStruct{} + BPMConfig = BPMConfigStruct{ + CleanupMakeDependencies: true, + } err = yaml.Unmarshal(bytes, &BPMConfig) if err != nil { return 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 i := len(BPMConfig.Databases) - 1; i >= 0; i-- { + if BPMConfig.Databases[i].Disabled != nil && *BPMConfig.Databases[i].Disabled { + BPMConfig.Databases = append(BPMConfig.Databases[:i], BPMConfig.Databases[i+1:]...) } } diff --git a/src/bpmlib/databases.go b/src/bpmlib/databases.go new file mode 100644 index 0000000..6b14195 --- /dev/null +++ b/src/bpmlib/databases.go @@ -0,0 +1,281 @@ +package bpmlib + +import ( + "errors" + "fmt" + "gopkg.in/yaml.v3" + "io" + "net/http" + "net/url" + "os" + "path" + "strings" +) + +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 BPMConfig.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 BPMConfig.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 BPMConfig.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 BPMConfig.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 BPMConfig.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 index b9d193e..618251a 100644 --- a/src/bpmlib/errors.go +++ b/src/bpmlib/errors.go @@ -10,7 +10,7 @@ type PackageNotFoundErr struct { } func (e PackageNotFoundErr) Error() string { - return "The following packages were not found in any repositories: " + strings.Join(e.packages, ", ") + return "The following packages were not found in any databases: " + strings.Join(e.packages, ", ") } type DependencyNotFoundErr struct { @@ -18,7 +18,7 @@ type DependencyNotFoundErr struct { } func (e DependencyNotFoundErr) Error() string { - return "The following dependencies were not found in any repositories: " + strings.Join(e.dependencies, ", ") + return "The following dependencies were not found in any databases: " + strings.Join(e.dependencies, ", ") } type PackageConflictErr struct { diff --git a/src/bpmlib/general.go b/src/bpmlib/general.go index c88ac32..b3ba3ad 100644 --- a/src/bpmlib/general.go +++ b/src/bpmlib/general.go @@ -5,7 +5,9 @@ import ( "fmt" "log" "os" + "path" "slices" + "strings" ) type ReinstallMethod uint8 @@ -16,15 +18,16 @@ const ( ReinstallMethodAll ReinstallMethod = iota ) -// InstallPackages installs the specified packages into the given root directory by fetching them from repositories or directly from local bpm archives -func InstallPackages(rootDir string, installationReason InstallationReason, reinstallMethod ReinstallMethod, installOptionalDependencies, forceInstallation, verbose bool, packages ...string) (operation *BPMOperation, err error) { +// 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, - ForceInstallationReason: installationReason, + Actions: make([]OperationAction, 0), + UnresolvedDepends: make([]string, 0), + Changes: make(map[string]string), + RootDir: rootDir, + compiledPackages: make(map[string]string), } // Resolve packages @@ -35,21 +38,59 @@ func InstallPackages(rootDir string, installationReason InstallationReason, rein 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, - IsDependency: false, - BpmPackage: bpmpkg, + File: pkg, + InstallationReason: installationReason, + BpmPackage: bpmpkg, }) } else { - var entry *RepositoryEntry + var entry *BPMDatabaseEntry - if e, _, err := GetRepositoryEntry(pkg); err == nil { + if e, _, err := GetDatabaseEntry(pkg); err == nil { entry = e } else if isVirtual, p := IsVirtualPackage(pkg, rootDir); isVirtual { - entry, _, err = GetRepositoryEntry(p) + entry, _, err = GetDatabaseEntry(p) if err != nil { pkgsNotFound = append(pkgsNotFound, pkg) continue @@ -63,9 +104,20 @@ func InstallPackages(rootDir string, installationReason InstallationReason, rein 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{ - IsDependency: false, - RepositoryEntry: entry, + InstallationReason: installationReason, + DatabaseEntry: entry, }) } } @@ -108,16 +160,39 @@ func InstallPackages(rootDir string, installationReason InstallationReason, rein } } + // 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, verbose bool, packages ...string) (operation *BPMOperation, err error) { +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 @@ -139,7 +214,7 @@ func RemovePackages(rootDir string, removeUnusedPackagesOnly, cleanupDependencie // Do package cleanup if cleanupDependencies { - err := operation.Cleanup(verbose) + err := operation.Cleanup(true) if err != nil { return nil, fmt.Errorf("could not perform cleanup for operation: %s", err) } @@ -148,16 +223,17 @@ func RemovePackages(rootDir string, removeUnusedPackagesOnly, cleanupDependencie } // CleanupPackages finds packages installed as dependencies which are no longer required by the rest of the system in the given root directory -func CleanupPackages(rootDir string, verbose bool) (operation *BPMOperation, err error) { +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(verbose) + err = operation.Cleanup(cleanupMakeDepends) if err != nil { return nil, fmt.Errorf("could not perform cleanup for operation: %s", err) } @@ -165,9 +241,91 @@ func CleanupPackages(rootDir string, verbose bool) (operation *BPMOperation, 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 repositories + // Sync databases if syncDatabase { err := SyncDatabase(verbose) if err != nil { @@ -176,12 +334,16 @@ func UpdatePackages(rootDir string, syncDatabase bool, installOptionalDependenci 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) + // 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 @@ -191,11 +353,11 @@ func UpdatePackages(rootDir string, syncDatabase bool, installOptionalDependenci } operation = &BPMOperation{ - Actions: make([]OperationAction, 0), - UnresolvedDepends: make([]string, 0), - Changes: make(map[string]string), - RootDir: rootDir, - ForceInstallationReason: InstallationReasonUnknown, + Actions: make([]OperationAction, 0), + UnresolvedDepends: make([]string, 0), + Changes: make(map[string]string), + RootDir: rootDir, + compiledPackages: make(map[string]string), } // Search for packages @@ -203,11 +365,11 @@ func UpdatePackages(rootDir string, syncDatabase bool, installOptionalDependenci if slices.Contains(BPMConfig.IgnorePackages, pkg) { continue } - var entry *RepositoryEntry + 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 = GetRepositoryEntry(pkg); err != nil { + } else if entry, _, err = GetDatabaseEntry(pkg); err != nil { continue } @@ -218,8 +380,8 @@ func UpdatePackages(rootDir string, syncDatabase bool, installOptionalDependenci comparison := ComparePackageVersions(*entry.Info, *installedInfo) if comparison > 0 { operation.AppendAction(&FetchPackageAction{ - IsDependency: false, - RepositoryEntry: entry, + InstallationReason: GetInstallationReason(pkg, rootDir), + DatabaseEntry: entry, }) } } @@ -245,12 +407,12 @@ func UpdatePackages(rootDir string, syncDatabase bool, installOptionalDependenci // SyncDatabase syncs all databases declared in /etc/bpm.conf func SyncDatabase(verbose bool) (err error) { - for _, repo := range BPMConfig.Repositories { + for _, db := range BPMConfig.Databases { if verbose { - fmt.Printf("Fetching package database for repository (%s)...\n", repo.Name) + fmt.Printf("Fetching package database file for database (%s)...\n", db.Name) } - err := repo.SyncLocalDatabase() + err := db.SyncLocalDatabaseFile() if err != nil { return err } diff --git a/src/bpmlib/hooks.go b/src/bpmlib/hooks.go index a9bbff4..d6e6d23 100644 --- a/src/bpmlib/hooks.go +++ b/src/bpmlib/hooks.go @@ -99,7 +99,10 @@ func (hook *BPMHook) Execute(packageChanges map[string]string, verbose bool, roo // Get modified files slice modifiedFiles := make([]*PackageFileEntry, 0) for pkg := range packageChanges { - modifiedFiles = append(modifiedFiles, GetPackageFiles(pkg, rootDir)...) + if GetPackage(pkg, rootDir) != nil { + modifiedFiles = append(modifiedFiles, GetPackage(pkg, rootDir).PkgFiles...) + } + } // Check if any targets are met 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 index f072759..2e0b4a7 100644 --- a/src/bpmlib/operations.go +++ b/src/bpmlib/operations.go @@ -11,11 +11,11 @@ import ( ) type BPMOperation struct { - Actions []OperationAction - UnresolvedDepends []string - Changes map[string]string - RootDir string - ForceInstallationReason InstallationReason + Actions []OperationAction + UnresolvedDepends []string + Changes map[string]string + RootDir string + compiledPackages map[string]string } func (operation *BPMOperation) ActionsContainPackage(pkg string) bool { @@ -25,7 +25,7 @@ func (operation *BPMOperation) ActionsContainPackage(pkg string) bool { return true } } else if action.GetActionType() == "fetch" { - if action.(*FetchPackageAction).RepositoryEntry.Info.Name == pkg { + if action.(*FetchPackageAction).DatabaseEntry.Info.Name == pkg { return true } } else if action.GetActionType() == "remove" { @@ -57,7 +57,7 @@ func (operation *BPMOperation) InsertActionAt(index int, action OperationAction) operation.Changes[pkgInfo.Name] = "upgrade" } } else if action.GetActionType() == "fetch" { - pkgInfo := action.(*FetchPackageAction).RepositoryEntry.Info + pkgInfo := action.(*FetchPackageAction).DatabaseEntry.Info if !IsPackageInstalled(pkgInfo.Name, operation.RootDir) { operation.Changes[pkgInfo.Name] = "install" } else { @@ -76,7 +76,7 @@ func (operation *BPMOperation) RemoveAction(pkg, actionType string) { if a.GetActionType() == "install" { return a.(*InstallPackageAction).BpmPackage.PkgInfo.Name == pkg } else if a.GetActionType() == "fetch" { - return a.(*FetchPackageAction).RepositoryEntry.Info.Name == pkg + return a.(*FetchPackageAction).DatabaseEntry.Info.Name == pkg } else if a.GetActionType() == "remove" { return a.(*RemovePackageAction).BpmPackage.PkgInfo.Name == pkg } @@ -88,7 +88,7 @@ func (operation *BPMOperation) GetTotalDownloadSize() uint64 { var ret uint64 = 0 for _, action := range operation.Actions { if action.GetActionType() == "fetch" { - ret += action.(*FetchPackageAction).RepositoryEntry.DownloadSize + ret += action.(*FetchPackageAction).DatabaseEntry.DownloadSize } } return ret @@ -100,7 +100,7 @@ func (operation *BPMOperation) GetTotalInstalledSize() uint64 { if action.GetActionType() == "install" { ret += action.(*InstallPackageAction).BpmPackage.GetInstalledSize() } else if action.GetActionType() == "fetch" { - ret += action.(*FetchPackageAction).RepositoryEntry.InstalledSize + ret += action.(*FetchPackageAction).DatabaseEntry.InstalledSize } } return ret @@ -115,7 +115,7 @@ func (operation *BPMOperation) GetFinalActionSize(rootDir string) int64 { ret -= int64(GetPackage(action.(*InstallPackageAction).BpmPackage.PkgInfo.Name, rootDir).GetInstalledSize()) } } else if action.GetActionType() == "fetch" { - ret += int64(action.(*FetchPackageAction).RepositoryEntry.InstalledSize) + ret += int64(action.(*FetchPackageAction).DatabaseEntry.InstalledSize) } else if action.GetActionType() == "remove" { ret -= int64(action.(*RemovePackageAction).BpmPackage.GetInstalledSize()) } @@ -132,28 +132,28 @@ func (operation *BPMOperation) ResolveDependencies(reinstallDependencies, instal pkgInfo = action.BpmPackage.PkgInfo } else if value.GetActionType() == "fetch" { action := value.(*FetchPackageAction) - pkgInfo = action.RepositoryEntry.Info + pkgInfo = action.DatabaseEntry.Info } else { pos++ continue } - resolved, unresolved := pkgInfo.ResolveDependencies(&[]string{}, &[]string{}, pkgInfo.Type == "source", installOptionalDependencies, !reinstallDependencies, verbose, operation.RootDir) + resolved, unresolved := ResolveAllPackageDependenciesFromDatabases(pkgInfo, 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) { + for _, resolvedPkg := range resolved { + if !operation.ActionsContainPackage(resolvedPkg.PkgName) && resolvedPkg.PkgName != pkgInfo.Name { + if !reinstallDependencies && IsPackageInstalled(resolvedPkg.PkgName, operation.RootDir) { continue } - entry, _, err := GetRepositoryEntry(depend) + entry, _, err := GetDatabaseEntry(resolvedPkg.PkgName) if err != nil { - return errors.New("could not get repository entry for package (" + depend + ")") + return errors.New("could not get database entry for package (" + resolvedPkg.PkgName + ")") } operation.InsertActionAt(pos, &FetchPackageAction{ - IsDependency: true, - RepositoryEntry: entry, + InstallationReason: resolvedPkg.InstallationReason, + DatabaseEntry: entry, }) pos++ } @@ -173,7 +173,7 @@ func (operation *BPMOperation) RemoveNeededPackages() error { } for pkg, action := range removeActions { - dependants, err := action.BpmPackage.PkgInfo.GetDependants(operation.RootDir) + dependants, err := GetPackageDependants(action.BpmPackage.PkgInfo.Name, operation.RootDir) if err != nil { return errors.New("could not get dependant packages for package (" + pkg + ")") } @@ -191,7 +191,7 @@ func (operation *BPMOperation) RemoveNeededPackages() error { return nil } -func (operation *BPMOperation) Cleanup(verbose bool) error { +func (operation *BPMOperation) Cleanup(cleanupMakeDepends bool) error { // Get all installed packages installedPackageNames, err := GetInstalledPackages(operation.RootDir) if err != nil { @@ -227,9 +227,9 @@ func (operation *BPMOperation) Cleanup(verbose bool) error { } keepPackages = append(keepPackages, pkg.Name) - resolved, _ := pkg.ResolveDependencies(&[]string{}, &[]string{}, false, true, false, verbose, operation.RootDir) + resolved := pkg.GetAllDependencies(!cleanupMakeDepends, true, operation.RootDir) for _, value := range resolved { - if !slices.Contains(keepPackages, value) && slices.Contains(installedPackageNames, value) { + if !slices.Contains(keepPackages, value) { keepPackages = append(keepPackages, value) } } @@ -262,7 +262,7 @@ func (operation *BPMOperation) ReplaceObsoletePackages() { } else if value.GetActionType() == "fetch" { action := value.(*FetchPackageAction) - pkgInfo = action.RepositoryEntry.Info + pkgInfo = action.DatabaseEntry.Info } else { continue } @@ -287,7 +287,7 @@ func (operation *BPMOperation) CheckForConflicts() (map[string][]string, error) for i, value := range installedPackages { bpmpkg := GetPackage(value, operation.RootDir) if bpmpkg == nil { - return nil, errors.New(fmt.Sprintf("could not find installed package (%s)", value)) + return nil, fmt.Errorf("could not find installed package (%s)", value) } allPackages[i] = bpmpkg.PkgInfo } @@ -300,7 +300,7 @@ func (operation *BPMOperation) CheckForConflicts() (map[string][]string, error) allPackages = append(allPackages, pkgInfo) } else if value.GetActionType() == "fetch" { action := value.(*FetchPackageAction) - pkgInfo := action.RepositoryEntry.Info + pkgInfo := action.DatabaseEntry.Info allPackages = append(allPackages, pkgInfo) } else if value.GetActionType() == "remove" { action := value.(*RemovePackageAction) @@ -335,10 +335,16 @@ func (operation *BPMOperation) ShowOperationSummary() { 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" { - pkgInfo = value.(*FetchPackageAction).RepositoryEntry.Info + 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()) @@ -346,21 +352,32 @@ func (operation *BPMOperation) ShowOperationSummary() { } installedInfo := GetPackageInfo(pkgInfo.Name, operation.RootDir) - sourceInfo := "" + additionalInfo := "" + switch installationReason { + case InstallationReasonManual: + additionalInfo = "(Manual)" + case InstallationReasonDependency: + additionalInfo = "(Dependency)" + case InstallationReasonMakeDependency: + additionalInfo = "(Make dependency)" + default: + additionalInfo = "(Unknown)" + } + if pkgInfo.Type == "source" { - sourceInfo = "(From Source)" + additionalInfo += " (From Source)" } if installedInfo == nil { - fmt.Printf("%s: %s (Install) %s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), sourceInfo) + 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(), sourceInfo) + 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(), sourceInfo) + 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(), sourceInfo) + fmt.Printf("%s: %s (Reinstall) %s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), additionalInfo) } } } @@ -409,30 +426,68 @@ func (operation *BPMOperation) RunHooks(verbose bool) error { return nil } -func (operation *BPMOperation) Execute(verbose, force bool) error { - // Fetch packages from repositories +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 repositories...") + 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 } - 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)) + + // 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) } - 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, + + 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, + } } } } @@ -462,32 +517,69 @@ func (operation *BPMOperation) Execute(verbose, force bool) error { 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)) + 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 - isReinstall := IsPackageInstalled(bpmpkg.PkgInfo.Name, operation.RootDir) var err error - if value.IsDependency { - err = installPackage(value.File, operation.RootDir, verbose, true) + + // 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(value.File, operation.RootDir, verbose, force) + err = installPackage(fileToInstall, operation.RootDir, verbose, force) } if err != nil { - return errors.New(fmt.Sprintf("could not install package (%s): %s\n", bpmpkg.PkgInfo.Name, err)) + return fmt.Errorf("could not install package (%s): %s\n", bpmpkg.PkgInfo.Name, err) } - if operation.ForceInstallationReason != InstallationReasonUnknown && !value.IsDependency { - err := SetInstallationReason(bpmpkg.PkgInfo.Name, operation.ForceInstallationReason, 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)) - } - } else if value.IsDependency && !isReinstall { - err := SetInstallationReason(bpmpkg.PkgInfo.Name, InstallationReasonDependency, 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)) - } + + // 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) } } @@ -501,9 +593,10 @@ type OperationAction interface { } type InstallPackageAction struct { - File string - IsDependency bool - BpmPackage *BPMPackage + File string + InstallationReason InstallationReason + SplitPackageToInstall string + BpmPackage *BPMPackage } func (action *InstallPackageAction) GetActionType() string { @@ -511,8 +604,8 @@ func (action *InstallPackageAction) GetActionType() string { } type FetchPackageAction struct { - IsDependency bool - RepositoryEntry *RepositoryEntry + InstallationReason InstallationReason + DatabaseEntry *BPMDatabaseEntry } func (action *FetchPackageAction) GetActionType() string { diff --git a/src/bpmlib/packages.go b/src/bpmlib/packages.go index da57201..fd4a368 100644 --- a/src/bpmlib/packages.go +++ b/src/bpmlib/packages.go @@ -8,6 +8,7 @@ import ( version "github.com/knqyf263/go-rpm-version" "gopkg.in/yaml.v3" "io" + "log" "os" "os/exec" "path" @@ -15,6 +16,7 @@ import ( "sort" "strconv" "strings" + "syscall" ) type BPMPackage struct { @@ -23,21 +25,22 @@ type BPMPackage struct { } 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"` + 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 { @@ -68,12 +71,32 @@ 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" - InstallationReasonUnknown InstallationReason = "unknown" + InstallationReasonManual InstallationReason = "manual" + InstallationReasonDependency InstallationReason = "dependency" + InstallationReasonMakeDependency InstallationReason = "make_dependency" + InstallationReasonUnknown InstallationReason = "unknown" ) func ComparePackageVersions(info1, info2 PackageInfo) int { @@ -98,6 +121,8 @@ func GetInstallationReason(pkg, rootDir string) InstallationReason { return InstallationReasonManual } else if reason == "dependency" { return InstallationReasonDependency + } else if reason == "make_dependency" { + return InstallationReasonMakeDependency } return InstallationReasonUnknown } @@ -147,7 +172,6 @@ func ReadPackage(filename string) (*BPMPackage, error) { var pkgFiles []*PackageFileEntry if _, err := os.Stat(filename); os.IsNotExist(err) { - fmt.Println("a") return nil, err } @@ -218,6 +242,31 @@ func ReadPackage(filename string) (*BPMPackage, error) { }, 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 @@ -237,31 +286,7 @@ func ReadPackageScripts(filename string) (map[string]string, error) { 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" { + 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 @@ -385,7 +410,7 @@ func executePackageScripts(filename, rootDir string, operation packageOperation, } func ReadPackageInfo(contents string) (*PackageInfo, error) { - pkgInfo := PackageInfo{ + pkgInfo := &PackageInfo{ Name: "", Description: "", Version: "", @@ -401,11 +426,14 @@ func ReadPackageInfo(contents string) (*PackageInfo, error) { 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 == "" { @@ -419,13 +447,47 @@ func ReadPackageInfo(contents string) (*PackageInfo, error) { } 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], "/") + 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) + } } - return &pkgInfo, nil + + // 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 bool, pkgInfo *PackageInfo, rootDir string) string { +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 { @@ -450,7 +512,7 @@ func CreateReadableInfo(showArchitecture, showType, showPackageRelations bool, p appendArray("Make Dependencies", pkgInfo.MakeDepends) } appendArray("Optional dependencies", pkgInfo.OptionalDepends) - dependants, err := pkgInfo.GetDependants(rootDir) + dependants, err := GetPackageDependants(pkgInfo.Name, rootDir) if err == nil { appendArray("Dependant packages", dependants) } @@ -458,7 +520,28 @@ func CreateReadableInfo(showArchitecture, showType, showPackageRelations bool, p appendArray("Provided packages", pkgInfo.Provides) appendArray("Replaces packages", pkgInfo.Replaces) } - ret = append(ret, "Installation Reason: "+string(GetInstallationReason(pkgInfo.Name, rootDir))) + 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") } @@ -480,7 +563,7 @@ func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string) return err } - tarballFile, err := readTarballContent(filename, "files.tar.gz") + tarballFile, err := readTarballFile(filename, "files.tar.gz") if err != nil { return err } @@ -502,14 +585,30 @@ func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string) 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 _, err := os.Stat(extractFilename); err == nil { if verbose { - fmt.Println("Creating Directory: " + extractFilename) + 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 @@ -518,7 +617,7 @@ func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string) 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)") + fmt.Printf("Skipping File: %s (Containing directory is set to be kept during reinstalls/updates)\n", extractFilename) } skip = true continue @@ -526,7 +625,7 @@ func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string) } else { if header.Name == k { if verbose { - fmt.Println("Skipping File: " + extractFilename + " (File is configured to be kept during reinstalls/updates)") + fmt.Printf("Skipping File: %s (File is configured to be kept during reinstalls/updates)\n", extractFilename) } skip = true continue @@ -542,34 +641,45 @@ func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string) 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.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/"))) @@ -584,13 +694,14 @@ func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string) } } 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 } + + if verbose { + fmt.Println("Created Hard Link: " + extractFilename + " -> " + path.Join(rootDir, destination)) + } } defer archive.Close() defer file.Close() @@ -605,11 +716,17 @@ func installPackage(filename, rootDir string, verbose, force bool) error { 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 := GetPackageFiles(bpmpkg.PkgInfo.Name, rootDir) + fileEntries := GetPackage(bpmpkg.PkgInfo.Name, rootDir).PkgFiles sort.Slice(fileEntries, func(i, j int) bool { return fileEntries[i].Path < fileEntries[j].Path }) @@ -710,15 +827,10 @@ func installPackage(filename, rootDir string, verbose, force bool) error { 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" { - return errors.New("direct source package compilation in BPM has been temporarily removed and is being reworked on") - } else { - return errors.New("unknown package type: " + bpmpkg.PkgInfo.Type) + // Extract package files into rootDir + err = extractPackage(bpmpkg, verbose, filename, rootDir) + if err != nil { + return err } installedDir := path.Join(rootDir, "var/lib/bpm/installed/") @@ -738,7 +850,7 @@ func installPackage(filename, rootDir string, verbose, force bool) error { return err } - tarballFile, err := readTarballContent(filename, "pkg.files") + tarballFile, err := readTarballFile(filename, "pkg.files") if err != nil { return err } @@ -766,23 +878,30 @@ func installPackage(filename, rootDir string, verbose, force bool) error { return err } - scripts, err := ReadPackageScripts(filename) + // Save remove package scripts + packageScripts, 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")) + 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 } - _, err = f.WriteString(val) - if err != nil { - return err - } - err = f.Close() + + // Write script contents to file + _, err = f.WriteString(content) if err != nil { return err } + + // Close file + f.Close() } if !packageInstalled { @@ -796,315 +915,19 @@ func installPackage(filename, rootDir string, verbose, force bool) error { 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 + localPackageInformation[rootDir][bpmpkg.PkgInfo.Name] = bpmpkg + 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) GetDependants(rootDir string) ([]string, error) { - ret := make([]string, 0) - - pkgs, err := GetInstalledPackages(rootDir) - if err != nil { - return nil, errors.New("could not get installed packages") - } - for _, pkg := range pkgs { - bpmpkg := GetPackage(pkg, rootDir) - if bpmpkg == nil { - return nil, errors.New("package not found: " + pkg) - } - if bpmpkg.PkgInfo.Name == pkgInfo.Name { - continue - } - - dependencies := bpmpkg.PkgInfo.GetAllDependencies(false, true) - - if slices.Contains(dependencies, pkgInfo.Name) { - ret = append(ret, pkg) - continue - } - for _, vpkg := range pkgInfo.Provides { - if slices.Contains(dependencies, vpkg) { - ret = append(ret, pkg) - break - } - } - } - - return ret, nil -} - -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 && IsPackageProvided(depend, rootDir) { - continue - } - var err error - var entry *RepositoryEntry - entry, _, err = GetRepositoryEntry(depend) - if err != nil { - if entry = ResolveVirtualPackage(depend); entry == 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 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 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) @@ -1113,8 +936,26 @@ func removePackage(pkg string, verbose bool, rootDir string) error { return errors.New("could not get package info") } + // Executing pre_remove script + if _, err := os.Stat(path.Join(pkgDir, "pre_remove.sh")); err == nil { + cmd := exec.Command("/bin/bash", path.Join(pkgDir, "pre_remove.sh")) + 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_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)) + + err = cmd.Run() + if err != nil { + log.Printf("Warning: could not run pre_remove.sh package script: %s", err) + } + } + // Fetching and reversing package file entry list - fileEntries := GetPackageFiles(pkg, rootDir) + fileEntries := GetPackage(pkg, rootDir).PkgFiles sort.Slice(fileEntries, func(i, j int) bool { return fileEntries[i].Path < fileEntries[j].Path }) @@ -1193,27 +1034,14 @@ func removePackage(pkg string, verbose bool, rootDir string) error { 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 + log.Printf("Warning: could not run pre_remove.sh package script: %s", err) } } @@ -1226,5 +1054,14 @@ func removePackage(pkg string, verbose bool, rootDir string) error { 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/repositories.go b/src/bpmlib/repositories.go deleted file mode 100644 index c74e440..0000000 --- a/src/bpmlib/repositories.go +++ /dev/null @@ -1,235 +0,0 @@ -package bpmlib - -import ( - "errors" - "fmt" - "gopkg.in/yaml.v3" - "io" - "net/http" - "net/url" - "os" - "path" - "strings" -) - -type Repository struct { - Name string `yaml:"name"` - Source string `yaml:"source"` - Disabled *bool `yaml:"disabled"` - Entries map[string]*RepositoryEntry - VirtualPackages map[string][]string -} - -type RepositoryEntry struct { - Info *PackageInfo `yaml:"info"` - Download string `yaml:"download"` - DownloadSize uint64 `yaml:"download_size"` - InstalledSize uint64 `yaml:"installed_size"` - 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 - } - - 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, - Repository: repo, - } - err := yaml.Unmarshal([]byte(b), &entry) - if err != nil { - return err - } - - for _, p := range entry.Info.Provides { - repo.VirtualPackages[p] = append(repo.VirtualPackages[p], entry.Info.Name) - } - repo.Entries[entry.Info.Name] = &entry - } - - return nil -} - -func (repo *Repository) SyncLocalDatabase() error { - repoFile := "/var/lib/bpm/repositories/" + repo.Name + ".bpmdb" - - // Get URL to database - u, err := url.JoinPath(repo.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 repository - err = yaml.Unmarshal(buffer, &Repository{}) - if err != nil { - return fmt.Errorf("could not decode repository: %s", err) - } - - // Create parent directories to repository file - err = os.MkdirAll(path.Dir(repoFile), 0755) - if err != nil { - return err - } - - // Create file and save repository data - out, err := os.Create(repoFile) - if err != nil { - return err - } - defer out.Close() - - _, err = out.Write(buffer) - - return nil -} - -func ReadLocalDatabases() (err error) { - for _, repo := range BPMConfig.Repositories { - // Initialize struct values - repo.Entries = make(map[string]*RepositoryEntry) - repo.VirtualPackages = make(map[string][]string) - - // Read database - err = repo.ReadLocalDatabase() - if err != nil { - return err - } - } - - 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 FindReplacement(pkg string) *RepositoryEntry { - for _, repo := range BPMConfig.Repositories { - for _, entry := range repo.Entries { - for _, replaced := range entry.Info.Replaces { - if replaced == pkg { - return entry - } - } - } - } - - return nil -} - -func ResolveVirtualPackage(vpkg string) *RepositoryEntry { - for _, repo := range BPMConfig.Repositories { - if v, ok := repo.VirtualPackages[vpkg]; ok { - for _, pkg := range v { - return repo.Entries[pkg] - } - } - } - - return nil -} - -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 -} diff --git a/src/bpmlib/tarball.go b/src/bpmlib/tarball.go index 84a0008..df9876d 100644 --- a/src/bpmlib/tarball.go +++ b/src/bpmlib/tarball.go @@ -5,6 +5,8 @@ import ( "errors" "io" "os" + "path" + "strings" ) type tarballFileReader struct { @@ -12,7 +14,35 @@ type tarballFileReader struct { file *os.File } -func readTarballContent(tarballPath, fileToExtract string) (*tarballFileReader, error) { +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 @@ -41,3 +71,139 @@ func readTarballContent(tarballPath, fileToExtract string) (*tarballFileReader, 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/src/bpmlib/utils.go b/src/bpmlib/utils.go index a89dff8..99275a0 100644 --- a/src/bpmlib/utils.go +++ b/src/bpmlib/utils.go @@ -2,9 +2,7 @@ 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 {