Compare commits

...

75 Commits

Author SHA1 Message Date
458c091ac2 Add "make_dependency" installation reason 2025-05-23 14:16:52 +03:00
966d351a80 Readd make depends field to binary packages 2025-05-15 16:07:15 +03:00
815d0bb29a Rename BPM Repositories to BPM Databases 2025-05-14 16:33:30 +03:00
bf2b4e95ac Fix installation reason showing in 'info' subcommand when viewing not-installed package, database entry or BPM file 2025-05-14 15:01:28 +03:00
d26d878308 Fix error when installing, updating or remove packages 2025-05-13 20:44:16 +03:00
f20d7b66d1 Remove unused verbose parameters 2025-05-13 20:33:20 +03:00
5f03a6a1ad Redo dependency resolution and cache installed package information 2025-05-13 20:26:55 +03:00
f2bc7c8968 Fix not reading local databases after reloading config during package update 2025-05-09 14:36:08 +03:00
97d5a06ce7 Remove force setting of URL field in split packages 2025-05-08 15:12:06 +03:00
f0709ff410 Fix 'prepare' function running in source directory during compilation 2025-05-08 12:44:55 +03:00
a32dfba54a Allow split package to have same name as main package 2025-05-04 21:03:05 +03:00
b07df8c7df 'compile' subcommand will now detect if virtual package is installed 2025-05-03 17:30:27 +03:00
1329109810 Add cache cleanup to 'cleanup' subcommand 2025-05-03 12:38:29 +03:00
4a8c4e22ec Move directories 2025-05-03 10:47:32 +03:00
eb8db1bc99 Fix output filename for source package compilation 2025-05-02 21:29:07 +03:00
4f1eeeb11d Fix dependency installation command arguments for 'bpm compile -d' 2025-05-02 21:20:56 +03:00
2e416b9e6f Allow direct split source package installation through repository entries 2025-05-02 20:43:24 +03:00
d8a42c780d Allow direct split source package installation through local BPM archive 2025-05-01 16:40:42 +03:00
b1bb8de661 Fix packages not compiling without root permissions 2025-04-27 12:04:30 +03:00
c425b263fe Fix cleanup flags not being read 2025-04-26 16:28:24 +03:00
5776784021 Remove no longer used flags from help message 2025-04-26 16:23:09 +03:00
903c7dce3e Change -o flag from output filename to output directory 2025-04-26 16:20:40 +03:00
5085981f52 Allow split source package compilation 2025-04-26 15:29:45 +03:00
9cdb3d29aa Reallow direct source package installation using 'bpm install' 2025-04-25 14:13:12 +03:00
d8146cb3f3 Remove unused function 2025-04-24 18:03:57 +03:00
c8c1fded76 Set output package architecture after compilation 2025-04-24 18:03:32 +03:00
4bf9ac9c60 Add .compilation-options file support 2025-04-24 17:39:17 +03:00
b73519ba9c Fix shell commands used in source package compilation 2025-04-24 16:44:37 +03:00
382a3fc8b6 Add flags for 'compile' subcommand 2025-04-24 13:56:30 +03:00
1843bceef5 Fix pre_remove.sh package script not being read or run 2025-04-22 19:28:36 +03:00
6af3b77d69 Include package scripts in compilation 2025-04-22 19:10:53 +03:00
8b6ef5fa63 Added compilation environment array to BPM config 2025-04-22 17:41:37 +03:00
e8d5f0a565 Add basic compilation functionality 2025-04-22 17:04:47 +03:00
7b0a8bf1d6 Merge pull request 'Reorganize codebase' (#10) from code_reorganization into develop
Reviewed-on: #10
2025-04-17 11:49:51 +00:00
3a62f23db5 Update README.md 2025-04-17 14:04:11 +03:00
d9d85b4943 Simplify Makefile and make it posix compliant 2025-04-17 13:58:49 +03:00
877fba8767 Use install command in Makefile, remove compress and run target and add uninstall target 2025-04-17 13:49:41 +03:00
0858a49636 Add .gitignore 2025-04-16 20:15:46 +03:00
b27137da29 Allow sync and update subcommands when local database files are corrupt 2025-04-16 20:13:22 +03:00
78dce34b64 Hide make dependencies when reading info of binary packages 2025-04-16 20:08:24 +03:00
f74372f13b Add 'ignore_packages' field to default config 2025-04-16 19:02:55 +03:00
dd41369e05 Fix error when hooks directory does not exist 2025-04-16 18:49:54 +03:00
fa3f59d8ba Update bpm.conf 2025-04-10 15:31:36 +03:00
46f7d96acf Update README.md 2025-04-10 15:29:44 +03:00
2f8d6a7ea9 Add missing dashes in the printHelp function 2025-04-10 15:08:27 +03:00
c767f26937 Remove direct source compilation (To be reworked) 2025-04-10 15:07:28 +03:00
3f2d5cf7a1 Remove all log.Fatal and os.Exit calls from bpmlib 2025-04-10 14:54:41 +03:00
14fe71842e Added custom error types 2025-04-10 14:38:32 +03:00
d1a2d28cff Fix some error handling and comments 2025-04-10 13:06:18 +03:00
151de2112e Fix virtual packages not being accounted for in GetDependants function 2025-04-08 19:20:49 +03:00
e60381beb1 Rename constants 2025-04-08 17:41:14 +03:00
0801612166 Create functions for basic package management in bpmlib 2025-04-07 21:03:33 +03:00
9485248d8e Unexport functions 2025-04-07 17:28:26 +03:00
68291e2666 Rename go files 2025-04-07 17:14:04 +03:00
05f273d687 Update bpm frontend module name 2025-04-07 17:10:19 +03:00
fcb2ef1515 Split project into the BPM frontend (src/bpm) and the BPM backend module (src/bpmlib) 2025-04-07 17:06:40 +03:00
97de746a7d Move go code to src/ subdirectory 2025-04-07 15:35:18 +03:00
a26737859c Merge pull request 'Add hook functionality' (#9) from hooks into develop
Reviewed-on: #9
2025-04-07 12:20:24 +00:00
90d5a23fcc Remove redundant underscore 2025-04-07 14:59:28 +03:00
7104441727 Allow globstar in bpmhook path targets 2025-04-07 14:58:56 +03:00
5f8e4f00ea Added basic hook functionality 2025-04-05 15:03:00 +03:00
87c492a30c Improve virtual package handling 2025-04-04 21:40:21 +03:00
e94b2a8816 Add replaces field to package information 2025-03-15 11:20:33 +02:00
4f9d2cdecd Add ability to ignore packages 2025-03-13 15:34:43 +02:00
e2e2629dc9 Make bash the default shell in Makefile 2025-03-13 15:34:10 +02:00
c3981ad95c Added repository entry information flag for the info subcommand and slightly altered the search subcommand format 2024-11-17 18:02:54 +02:00
852e615806 Changed info subcommand format and added support for checking .bpm file information 2024-11-17 17:32:49 +02:00
e285fd792e Fixed bug where dependencies would be installed with 'manual' as installation reason 2024-11-06 09:06:46 +02:00
2accc30390 Added --installation-reason flag 2024-11-05 15:34:55 +02:00
b625fe97ef Added --cleanup flag to the remove subcommand 2024-11-04 19:38:54 +02:00
6e2bb86ef0 Show dependant packages in the 'info' subcommand 2024-11-04 14:05:38 +02:00
e30a32c568 Added an --unused flag for the remove subcommand and remove will no longer display errors if a package does not exist 2024-10-30 16:35:52 +02:00
a2da73e26b Added a 'cleanup' subcommand that removes unused dependencies 2024-10-24 12:44:43 +03:00
2c7c4eeb73 Fixed small bug regarding installation reasons not always being read correctly 2024-10-23 19:13:18 +03:00
6e11f937a6 Added conflict checking 2024-10-23 16:10:42 +03:00
28 changed files with 4588 additions and 2902 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# IDE Directories
.idea
# Build directory
build

View File

@ -1,36 +1,30 @@
ifeq ($(PREFIX),)
PREFIX := /usr/local
endif
ifeq ($(BINDIR),)
BINDIR := $(PREFIX)/bin
endif
ifeq ($(SYSCONFDIR),)
SYSCONFDIR := $(PREFIX)/etc
endif
ifeq ($(GO),)
GO := $(shell type -a -P go | head -n 1)
endif
# Installation paths
PREFIX ?= /usr/local
BINDIR ?= $(PREFIX)/bin
SYSCONFDIR := $(PREFIX)/etc
# Compilers and tools
GO ?= $(shell which go)
# Build-time variables
ROOT_COMPILATION_UID ?= 65534
ROOT_COMPILATION_GID ?= 65534
build:
mkdir -p build
$(GO) build -ldflags "-w" -o build/bpm gitlab.com/bubble-package-manager/bpm
cd src/bpm; $(GO) build -ldflags "-w -X 'git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib.rootCompilationUID=$(ROOT_COMPILATION_UID)' -X 'git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib.rootCompilationGID=$(ROOT_COMPILATION_GID)'" -o ../../build/bpm git.enumerated.dev/bubble-package-manager/bpm/src/bpm
install: build/bpm config/
mkdir -p $(DESTDIR)$(BINDIR)
mkdir -p $(DESTDIR)$(SYSCONFDIR)
cp build/bpm $(DESTDIR)$(BINDIR)/bpm
cp config/bpm.conf $(DESTDIR)$(SYSCONFDIR)/bpm.conf
# Create directories
install -dm755 $(DESTDIR)$(BINDIR)
install -dm755 $(DESTDIR)$(SYSCONFDIR)
# Install files
install -Dm755 build/bpm $(DESTDIR)$(BINDIR)/bpm
install -Dm644 config/bpm.conf $(DESTDIR)$(SYSCONFDIR)/bpm.conf
compress: build/bpm config/
mkdir -p bpm/$(BINDIR)
mkdir -p bpm/$(SYSCONFDIR)
cp build/bpm bpm/$(BINDIR)/bpm
cp config/bpm.conf bpm/$(SYSCONFDIR)/bpm.conf
tar --owner=root --group=root -czf bpm.tar.gz bpm
rm -r bpm
run: build/bpm
build/bpm
uninstall:
rm $(DESTDIR)$(BINDIR)/bpm
rm $(DESTDIR)$(SYSCONFDIR)/bpm.conf
clean:
rm -r build/

View File

@ -5,24 +5,22 @@ BPM is a simple package manager for Linux systems
## Features
- Simple to use subcommands
- Can install binary and source packages
- Can be easily installed on practically any system
- No bloat
- Can install binary packages (and source packages in the future)
## Information
BPM is still in very early development. It should not be installed on any system you use seriously. I recommend trying this out in a VM or container. In addition to this, this is one of the first projects I have made using the go programming language so code quality may not be the best. This project was made to help me learn go and how linux systems work better. It is not meant to replace the big package managers in any way
BPM is still in very early development. Do not install it without knowing what you are doing. I would only recommend using it in a Virtual Machine for testing
## Build from source
- Download `go` from your package manager or from the go website
- Download `make` from your package manager
- Run the following command to compile the project
```
- 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
```
- Run the following command to install stormfetch into your system. You may also append a DESTDIR variable at the end of this line if you wish to install in a different location
```
- Run the following command to install BPM into your system. You may also append a DESTDIR variable at the end of this line if you wish to install in a different location
```sh
make install PREFIX=/usr SYSCONFDIR=/etc
```
@ -32,24 +30,38 @@ You are able to install bpm packages by typing the following:
```sh
bpm install /path/to/package.bpm
```
You may also use the -y flag as shown below to bypass the installation confirmation prompt
You can also use the package name directly if using databases
```sh
bpm install package_name
```
The -y flag may be used as shown below to bypass the confirmation prompt
```sh
bpm install -y /path/to/package.bpm
```
Flags must strictly be typed before the first package path otherwise they'll be read as package locations themselves
Flags must strictly be typed before the first package path or name, otherwise they'll be read as package locations themselves
You can remove an installed package by typing the following
```sh
bpm remove package_name
```
The -y flag applies here as well if you wish to skip the removal confirmation prompt
To remove all unused dependencies and clean cached files try using the cleanup command
```sh
bpm cleanup
```
If using databases, all packages can be updated using this simple command
```sh
bpm update
```
For information on the rest of the commands simply use the help command or pass in no arguments at all
```
```sh
bpm help
```
## Package Creation
Package creation is simplified using the bpm-utils package which contains helper scripts for creating and archiving packages. \
Learn more here: https://gitlab.com/bubble-package-manager/bpm-utils
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

View File

@ -1,8 +1,8 @@
ignore_packages: []
privilege_escalator_cmd: "sudo"
compilation_env: []
silent_compilation: false
compilation_dir: "/var/tmp/"
binary_output_dir: "/var/lib/bpm/compiled/"
repositories:
- name: example-repository
source: https://my-repo.xyz/
cleanup_make_dependencies: true
databases:
- name: example-database
source: https://my-database.xyz/
disabled: true

9
go.mod
View File

@ -1,9 +0,0 @@
module gitlab.com/bubble-package-manager/bpm
go 1.22
require (
github.com/elliotchance/orderedmap/v2 v2.4.0 // indirect
github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

599
main.go
View File

@ -1,599 +0,0 @@
package main
import (
"bufio"
"flag"
"fmt"
"gitlab.com/bubble-package-manager/bpm/utils"
"log"
"os"
"path/filepath"
"slices"
"strings"
)
/* -------------BPM | Bubble Package Manager-------------- */
/* Made By EnumDev (Previously CapCreeperGR) */
/* A simple-to-use package manager */
/* ------------------------------------------------------- */
var bpmVer = "0.5.0"
var subcommand = "help"
var subcommandArgs []string
// Flags
var rootDir = "/"
var verbose = false
var yesAll = false
var buildSource = false
var skipCheck = false
var keepTempDir = false
var force = false
var pkgListNumbers = false
var pkgListNames = false
var reinstall = false
var reinstallAll = false
var noOptional = false
var nosync = true
func main() {
utils.ReadConfig()
resolveFlags()
resolveCommand()
}
type commandType uint8
const (
_default commandType = iota
help
info
list
search
install
update
sync
remove
file
)
func getCommandType() commandType {
switch subcommand {
case "version":
return _default
case "info":
return info
case "list":
return list
case "search":
return search
case "install":
return install
case "update":
return update
case "sync":
return sync
case "remove":
return remove
case "file":
return file
default:
return help
}
}
func resolveCommand() {
switch getCommandType() {
case _default:
fmt.Println("Bubble Package Manager (BPM)")
fmt.Println("Version: " + bpmVer)
case info:
packages := subcommandArgs
if len(packages) == 0 {
fmt.Println("No packages were given")
return
}
for n, pkg := range packages {
var info *utils.PackageInfo
info = utils.GetPackageInfo(pkg, rootDir)
if info == nil {
log.Fatalf("Error: package (%s) is not installed\n", pkg)
}
fmt.Println("----------------")
fmt.Println(utils.CreateReadableInfo(true, true, true, info, rootDir))
if n == len(packages)-1 {
fmt.Println("----------------")
}
}
case list:
packages, err := utils.GetInstalledPackages(rootDir)
if err != nil {
log.Fatalf("Error: could not get installed packages: %s", err.Error())
return
}
if pkgListNumbers {
fmt.Println(len(packages))
} else if pkgListNames {
for _, pkg := range packages {
fmt.Println(pkg)
}
} else {
if len(packages) == 0 {
fmt.Println("No packages have been installed")
return
}
for n, pkg := range packages {
info := utils.GetPackageInfo(pkg, rootDir)
if info == nil {
fmt.Printf("Package (%s) could not be found\n", pkg)
continue
}
fmt.Println("----------------\n" + utils.CreateReadableInfo(true, true, true, info, rootDir))
if n == len(packages)-1 {
fmt.Println("----------------")
}
}
}
case search:
searchTerms := subcommandArgs
if len(searchTerms) == 0 {
log.Fatalf("Error: no search terms given")
}
for _, term := range searchTerms {
nameResults := make([]*utils.PackageInfo, 0)
descResults := make([]*utils.PackageInfo, 0)
for _, repo := range utils.BPMConfig.Repositories {
for _, entry := range repo.Entries {
if strings.Contains(entry.Info.Name, term) {
nameResults = append(nameResults, entry.Info)
} else if strings.Contains(entry.Info.Description, term) {
descResults = append(descResults, entry.Info)
}
}
}
results := append(nameResults, descResults...)
if len(results) == 0 {
log.Fatalf("Error: no results for term (%s) were found\n", term)
}
fmt.Printf("Results for term (%s)\n", term)
for i, result := range results {
fmt.Println("----------------")
fmt.Printf("%d) %s: %s (%s)\n", i+1, result.Name, result.Description, result.GetFullVersion())
}
}
case install:
if os.Getuid() != 0 {
log.Fatalf("Error: this subcommand needs to be run with superuser permissions")
}
pkgs := subcommandArgs
if len(pkgs) == 0 {
fmt.Println("No packages or files were given to install")
return
}
operation := utils.BPMOperation{
Actions: make([]utils.OperationAction, 0),
UnresolvedDepends: make([]string, 0),
RootDir: rootDir,
}
// Search for packages
for _, pkg := range pkgs {
if stat, err := os.Stat(pkg); err == nil && !stat.IsDir() {
bpmpkg, err := utils.ReadPackage(pkg)
if err != nil {
log.Fatalf("Error: could not read package: %s\n", err)
}
if !reinstall && utils.IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) && utils.GetPackageInfo(bpmpkg.PkgInfo.Name, rootDir).GetFullVersion() == bpmpkg.PkgInfo.GetFullVersion() {
continue
}
operation.Actions = append(operation.Actions, &utils.InstallPackageAction{
File: pkg,
IsDependency: false,
BpmPackage: bpmpkg,
})
} else {
entry, _, err := utils.GetRepositoryEntry(pkg)
if err != nil {
log.Fatalf("Error: could not find package (%s) in any repository\n", pkg)
}
if !reinstall && utils.IsPackageInstalled(entry.Info.Name, rootDir) && utils.GetPackageInfo(entry.Info.Name, rootDir).GetFullVersion() == entry.Info.GetFullVersion() {
continue
}
operation.Actions = append(operation.Actions, &utils.FetchPackageAction{
IsDependency: false,
RepositoryEntry: entry,
})
}
}
// Resolve dependencies
err := operation.ResolveDependencies(reinstallAll, !noOptional, verbose)
if err != nil {
log.Fatalf("Error: could not resolve dependencies: %s\n", err)
}
if len(operation.UnresolvedDepends) != 0 {
if !force {
log.Fatalf("Error: the following dependencies could not be found in any repositories: %s\n", strings.Join(operation.UnresolvedDepends, ", "))
} else {
log.Println("Warning: The following dependencies could not be found in any repositories: " + strings.Join(operation.UnresolvedDepends, ", "))
}
}
// Show operation summary
operation.ShowOperationSummary()
// Confirmation Prompt
if !yesAll {
reader := bufio.NewReader(os.Stdin)
if len(operation.Actions) == 1 {
fmt.Printf("Do you wish to install this package? [y\\N] ")
} else {
fmt.Printf("Do you wish to install these %d packages? [y\\N] ", len(operation.Actions))
}
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Println("Cancelling package installation...")
os.Exit(1)
}
}
// Execute operation
err = operation.Execute(verbose, force)
if err != nil {
log.Fatalf("Error: could not complete operation: %s\n", err)
}
case update:
if os.Getuid() != 0 {
log.Fatalf("Error: this subcommand needs to be run with superuser permissions")
}
// Sync repositories
if !nosync {
for _, repo := range utils.BPMConfig.Repositories {
fmt.Printf("Fetching package database for repository (%s)...\n", repo.Name)
err := repo.SyncLocalDatabase()
if err != nil {
log.Fatalf("Error: could not sync local database for repository (%s): %s\n", repo.Name, err)
}
}
fmt.Println("All package databases synced successfully!")
}
utils.ReadConfig()
// Get installed packages and check for updates
pkgs, err := utils.GetInstalledPackages(rootDir)
if err != nil {
log.Fatalf("Error: could not get installed packages: %s\n", err)
}
operation := utils.BPMOperation{
Actions: make([]utils.OperationAction, 0),
UnresolvedDepends: make([]string, 0),
RootDir: rootDir,
}
// Search for packages
for _, pkg := range pkgs {
entry, _, err := utils.GetRepositoryEntry(pkg)
if err != nil {
continue
}
installedInfo := utils.GetPackageInfo(pkg, rootDir)
if installedInfo == nil {
log.Fatalf("Error: could not get package info for (%s)\n", pkg)
} else {
comparison := utils.ComparePackageVersions(*entry.Info, *installedInfo)
if comparison > 0 || reinstall {
operation.Actions = append(operation.Actions, &utils.FetchPackageAction{
IsDependency: false,
RepositoryEntry: entry,
})
}
}
}
// Check for new dependencies in updated packages
err = operation.ResolveDependencies(reinstallAll, !noOptional, verbose)
if err != nil {
log.Fatalf("Error: could not resolve dependencies: %s\n", err)
}
if len(operation.UnresolvedDepends) != 0 {
if !force {
log.Fatalf("Error: the following dependencies could not be found in any repositories: %s\n", strings.Join(operation.UnresolvedDepends, ", "))
} else {
log.Println("Warning: The following dependencies could not be found in any repositories: " + strings.Join(operation.UnresolvedDepends, ", "))
}
}
// Show operation summary
operation.ShowOperationSummary()
// Confirmation Prompt
if !yesAll {
fmt.Printf("Are you sure you wish to update all %d packages? [y\\N] ", len(operation.Actions))
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Println("Cancelling package update...")
os.Exit(1)
}
}
// Execute operation
err = operation.Execute(verbose, force)
if err != nil {
log.Fatalf("Error: could not complete operation: %s\n", err)
}
case sync:
if os.Getuid() != 0 {
log.Fatalf("Error: this subcommand needs to be run with superuser permissions")
}
if !yesAll {
fmt.Printf("Are you sure you wish to sync all databases? [y\\N] ")
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Println("Cancelling database synchronization...")
os.Exit(1)
}
}
for _, repo := range utils.BPMConfig.Repositories {
fmt.Printf("Fetching package database for repository (%s)...\n", repo.Name)
err := repo.SyncLocalDatabase()
if err != nil {
log.Fatalf("Error: could not sync local database for repository (%s): %s\n", repo.Name, err)
}
}
fmt.Println("All package databases synced successfully!")
case remove:
if os.Getuid() != 0 {
log.Fatalf("Error: this subcommand needs to be run with superuser permissions")
}
packages := subcommandArgs
if len(packages) == 0 {
fmt.Println("No packages were given")
return
}
operation := &utils.BPMOperation{
Actions: make([]utils.OperationAction, 0),
UnresolvedDepends: make([]string, 0),
RootDir: rootDir,
}
// Search for packages
for _, pkg := range packages {
bpmpkg := utils.GetPackage(pkg, rootDir)
if bpmpkg == nil {
log.Fatalf("Error: package (%s) could not be found\n", pkg)
}
operation.Actions = append(operation.Actions, &utils.RemovePackageAction{BpmPackage: bpmpkg})
}
// Show operation summary
operation.ShowOperationSummary()
// Confirmation Prompt
if !yesAll {
fmt.Printf("Are you sure you wish to remove all %d packages? [y\\N] ", len(operation.Actions))
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Println("Cancelling package removal...")
os.Exit(1)
}
}
// Execute operation
err := operation.Execute(verbose, force)
if err != nil {
log.Fatalf("Error: could not complete operation: %s\n", err)
}
case file:
files := subcommandArgs
if len(files) == 0 {
fmt.Println("No files were given to get which packages manage it")
return
}
for _, file := range files {
absFile, err := filepath.Abs(file)
if err != nil {
log.Fatalf("Error: could not get absolute path of file (%s)\n", file)
}
stat, err := os.Stat(absFile)
if os.IsNotExist(err) {
log.Fatalf("Error: file (%s) does not exist!\n", absFile)
}
pkgs, err := utils.GetInstalledPackages(rootDir)
if err != nil {
log.Fatalf("Error: could not get installed packages: %s\n", err.Error())
}
if !strings.HasPrefix(absFile, rootDir) {
log.Fatalf("Error: could not get path of file (%s) relative to root path", absFile)
}
absFile, err = filepath.Rel(rootDir, absFile)
if err != nil {
log.Fatalf("Error: could not get path of file (%s) relative to root path", absFile)
}
absFile = strings.TrimPrefix(absFile, "/")
if stat.IsDir() {
absFile = absFile + "/"
}
var pkgList []string
for _, pkg := range pkgs {
if slices.ContainsFunc(utils.GetPackageFiles(pkg, rootDir), func(entry *utils.PackageFileEntry) bool {
return entry.Path == absFile
}) {
pkgList = append(pkgList, pkg)
}
}
if len(pkgList) == 0 {
fmt.Println(absFile + " is not managed by any packages")
} else {
fmt.Println(absFile + " is managed by the following packages:")
for _, pkg := range pkgList {
fmt.Println("- " + pkg)
}
}
}
default:
printHelp()
}
}
func printHelp() {
fmt.Println("\033[1m---- Command Format ----\033[0m")
fmt.Println("-> command format: bpm <subcommand> [-flags]...")
fmt.Println("-> flags will be read if passed right after the subcommand otherwise they will be read as subcommand arguments")
fmt.Println("\033[1m---- Command List ----\033[0m")
fmt.Println("-> bpm version | shows information on the installed version of bpm")
fmt.Println("-> bpm info [-R] <packages...> | shows information on an installed package")
fmt.Println(" -R=<path> lets you define the root path which will be used")
fmt.Println("-> bpm list [-R, -c, -n] | lists all installed packages")
fmt.Println(" -R=<path> 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 <search terms...> | Searches for packages through declared repositories")
fmt.Println("-> bpm install [-R, -v, -y, -f, -o, -c, -b, -k, --reinstall, --reinstall-all, --no-optional] <packages...> | installs the following files")
fmt.Println(" -R=<path> lets you define the root path which will be used")
fmt.Println(" -v Show additional information about what BPM is doing")
fmt.Println(" -y skips the confirmation prompt")
fmt.Println(" -f skips dependency, conflict and architecture checking")
fmt.Println(" -o=<path> set the binary package output directory (defaults to /var/lib/bpm/compiled)")
fmt.Println(" -c=<path> set the compilation directory (defaults to /var/tmp)")
fmt.Println(" -b creates a binary package from a source package after compilation and saves it in the binary package output directory")
fmt.Println(" -k keeps the compilation directory created by BPM after source package installation")
fmt.Println(" --reinstall Reinstalls packages even if they do not have a newer version available")
fmt.Println(" --reinstall-all Same as --reinstall but also reinstalls dependencies")
fmt.Println(" --no-optional Prevents installation of optional dependencies")
fmt.Println("-> bpm update [-R, -v, -y, -f, --reinstall, --no-sync] | updates all packages that are available in the repositories")
fmt.Println(" -R=<path> lets you define the root path which will be used")
fmt.Println(" -v Show additional information about what BPM is doing")
fmt.Println(" -y skips the confirmation prompt")
fmt.Println(" -f skips dependency, conflict and architecture checking")
fmt.Println(" --reinstall Fetches and reinstalls all packages even if they do not have a newer version available")
fmt.Println(" --no-sync Skips package database syncing")
fmt.Println("-> bpm sync [-R, -v, -y] | Syncs package databases without updating packages")
fmt.Println(" -R=<path> lets you define the root path which will be used")
fmt.Println(" -v Show additional information about what BPM is doing")
fmt.Println(" -y skips the confirmation prompt")
fmt.Println("-> bpm remove [-R, -v, -y] <packages...> | removes the following packages")
fmt.Println(" -v Show additional information about what BPM is doing")
fmt.Println(" -R=<path> lets you define the root path which will be used")
fmt.Println(" -y skips the confirmation prompt")
fmt.Println("-> bpm file [-R] <files...> | shows what packages the following packages are managed by")
fmt.Println(" -R=<root_path> lets you define the root path which will be used")
fmt.Println("\033[1m----------------\033[0m")
}
func resolveFlags() {
// List flags
listFlagSet := flag.NewFlagSet("List flags", flag.ExitOnError)
listFlagSet.Usage = printHelp
listFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
listFlagSet.BoolVar(&pkgListNumbers, "c", false, "List the number of all packages installed with BPM")
listFlagSet.BoolVar(&pkgListNames, "n", false, "List the names of all packages installed with BPM")
// Info flags
infoFlagSet := flag.NewFlagSet("Info flags", flag.ExitOnError)
infoFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
infoFlagSet.Usage = printHelp
// Install flags
installFlagSet := flag.NewFlagSet("Install flags", flag.ExitOnError)
installFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
installFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing")
installFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts")
installFlagSet.StringVar(&utils.BPMConfig.BinaryOutputDir, "o", utils.BPMConfig.BinaryOutputDir, "Set the binary output directory")
installFlagSet.StringVar(&utils.BPMConfig.CompilationDir, "c", utils.BPMConfig.CompilationDir, "Set the compilation directory")
installFlagSet.BoolVar(&buildSource, "b", false, "Build binary package from source package")
installFlagSet.BoolVar(&skipCheck, "s", false, "Skip check function during source compilation")
installFlagSet.BoolVar(&keepTempDir, "k", false, "Keep temporary directory after source compilation")
installFlagSet.BoolVar(&force, "f", false, "Force installation by skipping architecture and dependency resolution")
installFlagSet.BoolVar(&reinstall, "reinstall", false, "Reinstalls packages even if they do not have a newer version available")
installFlagSet.BoolVar(&reinstallAll, "reinstall-all", false, "Same as --reinstall but also reinstalls dependencies")
installFlagSet.BoolVar(&noOptional, "no-optional", false, "Prevents installation of optional dependencies")
installFlagSet.Usage = printHelp
// Update flags
updateFlagSet := flag.NewFlagSet("Update flags", flag.ExitOnError)
updateFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
updateFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing")
updateFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts")
updateFlagSet.BoolVar(&force, "f", false, "Force update by skipping architecture and dependency resolution")
updateFlagSet.BoolVar(&reinstall, "reinstall", false, "Fetches and reinstalls all packages even if they do not have a newer version available")
updateFlagSet.BoolVar(&nosync, "no-sync", false, "Skips package database syncing")
updateFlagSet.Usage = printHelp
// Sync flags
syncFlagSet := flag.NewFlagSet("Sync flags", flag.ExitOnError)
syncFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
syncFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing")
syncFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts")
syncFlagSet.Usage = printHelp
// Remove flags
removeFlagSet := flag.NewFlagSet("Remove flags", flag.ExitOnError)
removeFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
removeFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing")
removeFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts")
removeFlagSet.Usage = printHelp
// File flags
fileFlagSet := flag.NewFlagSet("Remove flags", flag.ExitOnError)
fileFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
fileFlagSet.Usage = printHelp
if len(os.Args[1:]) <= 0 {
subcommand = "help"
} else {
subcommand = os.Args[1]
subcommandArgs = os.Args[2:]
if getCommandType() == list {
err := listFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = listFlagSet.Args()
} else if getCommandType() == info {
err := infoFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = infoFlagSet.Args()
} else if getCommandType() == install {
err := installFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = installFlagSet.Args()
} else if getCommandType() == update {
err := updateFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = updateFlagSet.Args()
} else if getCommandType() == sync {
err := syncFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = syncFlagSet.Args()
} else if getCommandType() == remove {
err := removeFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = removeFlagSet.Args()
} else if getCommandType() == file {
err := fileFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = fileFlagSet.Args()
}
if reinstallAll {
reinstall = true
}
}
}

14
src/bpm/go.mod Normal file
View File

@ -0,0 +1,14 @@
module git.enumerated.dev/bubble-package-manager/bpm/src/bpm
go 1.23
toolchain go1.23.7
require git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib v0.5.0
replace git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib => ../bpmlib
require (
github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -1,9 +1,6 @@
github.com/elliotchance/orderedmap/v2 v2.4.0 h1:6tUmMwD9F998FNpwFxA5E6NQvSpk2PVw7RKsVq3+2Cw=
github.com/elliotchance/orderedmap/v2 v2.4.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f h1:xt29M2T6STgldg+WEP51gGePQCsQvklmP2eIhPIBK3g=
github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

949
src/bpm/main.go Normal file
View File

@ -0,0 +1,949 @@
package main
import (
"bufio"
"errors"
"flag"
"fmt"
"git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"slices"
"strings"
)
/* -------------BPM | Bubble Package Manager-------------- */
/* Made By EnumDev (Previously CapCreeperGR) */
/* A simple-to-use package manager */
/* ------------------------------------------------------- */
var bpmVer = "0.5.0"
var subcommand = "help"
var subcommandArgs []string
// Flags
var rootDir = "/"
var verbose = false
var yesAll = false
var force = false
var pkgListNumbers = false
var pkgListNames = false
var reinstall = false
var reinstallAll = false
var noOptional = false
var installationReason = ""
var nosync = true
var removeUnused = false
var doCleanup = false
var showDatabaseInfo = false
var installSrcPkgDepends = false
var skipChecks = false
var outputDirectory = ""
var cleanupDependencies = false
var cleanupMakeDependencies = false
var cleanupCompilationFiles = false
var cleanupCompiledPackages = false
var cleanupFetchedPackages = false
func main() {
err := bpmlib.ReadConfig()
if err != nil {
log.Fatalf("Error: could not read BPM config: %s", err)
}
resolveFlags()
resolveCommand()
}
type commandType uint8
const (
_default commandType = iota
help
info
list
search
install
update
sync
remove
cleanup
file
compile
)
func getCommandType() commandType {
switch subcommand {
case "version":
return _default
case "info":
return info
case "list":
return list
case "search":
return search
case "install":
return install
case "update":
return update
case "sync":
return sync
case "remove":
return remove
case "cleanup":
return cleanup
case "file":
return file
case "compile":
return compile
default:
return help
}
}
func resolveCommand() {
switch getCommandType() {
case _default:
fmt.Println("Bubble Package Manager (BPM)")
fmt.Println("Version: " + bpmVer)
case info:
packages := subcommandArgs
if len(packages) == 0 {
fmt.Println("No packages were given")
return
}
// Read local databases
err := bpmlib.ReadLocalDatabaseFiles()
if err != nil {
log.Fatalf("Error: could not read local databases: %s", err)
}
for n, pkg := range packages {
var info *bpmlib.PackageInfo
isFile := false
showInstallationReason := false
if showDatabaseInfo {
var err error
var entry *bpmlib.BPMDatabaseEntry
entry, _, err = bpmlib.GetDatabaseEntry(pkg)
if err != nil {
if entry = bpmlib.ResolveVirtualPackage(pkg); entry == nil {
log.Fatalf("Error: could not find package (%s) in any database\n", pkg)
}
}
info = entry.Info
} else if stat, err := os.Stat(pkg); err == nil && !stat.IsDir() {
bpmpkg, err := bpmlib.ReadPackage(pkg)
if err != nil {
log.Fatalf("Error: could not read package: %s\n", err)
}
info = bpmpkg.PkgInfo
isFile = true
} else {
if isVirtual, p := bpmlib.IsVirtualPackage(pkg, rootDir); isVirtual {
info = bpmlib.GetPackageInfo(p, rootDir)
} else {
info = bpmlib.GetPackageInfo(pkg, rootDir)
}
showInstallationReason = true
}
if info == nil {
log.Fatalf("Error: package (%s) is not installed\n", pkg)
}
if n != 0 {
fmt.Println()
}
if isFile {
abs, err := filepath.Abs(pkg)
if err != nil {
log.Fatalf("Error: could not get absolute path of file (%s)\n", abs)
}
fmt.Println("File: " + abs)
}
fmt.Println(bpmlib.CreateReadableInfo(true, true, true, showInstallationReason, info, rootDir))
}
case list:
// Read local databases
err := bpmlib.ReadLocalDatabaseFiles()
if err != nil {
log.Fatalf("Error: could not read local databases: %s", err)
}
packages, err := bpmlib.GetInstalledPackages(rootDir)
if err != nil {
log.Fatalf("Error: could not get installed packages: %s", err.Error())
return
}
if pkgListNumbers {
fmt.Println(len(packages))
} else if pkgListNames {
for _, pkg := range packages {
fmt.Println(pkg)
}
} else {
if len(packages) == 0 {
fmt.Println("No packages have been installed")
return
}
for n, pkg := range packages {
info := bpmlib.GetPackageInfo(pkg, rootDir)
if info == nil {
fmt.Printf("Package (%s) could not be found\n", pkg)
continue
}
if n != 0 {
fmt.Println()
}
fmt.Println(bpmlib.CreateReadableInfo(true, true, true, true, info, rootDir))
}
}
case search:
searchTerms := subcommandArgs
if len(searchTerms) == 0 {
log.Fatalf("Error: no search terms given")
}
// Read local databases
err := bpmlib.ReadLocalDatabaseFiles()
if err != nil {
log.Fatalf("Error: could not read local databases: %s", err)
}
for i, term := range searchTerms {
nameResults := make([]*bpmlib.PackageInfo, 0)
descResults := make([]*bpmlib.PackageInfo, 0)
for _, db := range bpmlib.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) {
descResults = append(descResults, entry.Info)
}
}
}
results := append(nameResults, descResults...)
if len(results) == 0 {
log.Fatalf("Error: no results for term (%s) were found\n", term)
}
if i > 0 {
fmt.Println()
}
fmt.Printf("Results for term (%s)\n", term)
for j, result := range results {
fmt.Printf("%d) %s: %s (%s)\n", j+1, result.Name, result.Description, result.GetFullVersion())
}
}
case install:
// Check for required permissions
if os.Getuid() != 0 {
log.Fatalf("Error: this subcommand needs to be run with superuser permissions")
}
// Return if no packages are specified
if len(subcommandArgs) == 0 {
fmt.Println("No packages or files were given to install")
return
}
// Check if installationReason argument is valid
ir := bpmlib.InstallationReasonManual
switch installationReason {
case "manual":
ir = bpmlib.InstallationReasonManual
case "dependency":
ir = bpmlib.InstallationReasonDependency
case "make-dependency":
ir = bpmlib.InstallationReasonMakeDependency
case "":
default:
log.Fatalf("Error: %s is not a valid installation reason", installationReason)
}
// Get reinstall method
var reinstallMethod bpmlib.ReinstallMethod
if reinstallAll {
reinstallMethod = bpmlib.ReinstallMethodAll
} else if reinstall {
reinstallMethod = bpmlib.ReinstallMethodSpecified
} else {
reinstallMethod = bpmlib.ReinstallMethodNone
}
// Read local databases
err := bpmlib.ReadLocalDatabaseFiles()
if err != nil {
log.Fatalf("Error: could not read local databases: %s", err)
}
// Create installation operation
operation, err := bpmlib.InstallPackages(rootDir, ir, reinstallMethod, !noOptional, force, verbose, subcommandArgs...)
if errors.As(err, &bpmlib.PackageNotFoundErr{}) || errors.As(err, &bpmlib.DependencyNotFoundErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) {
log.Fatalf("Error: %s", err)
} else if err != nil {
log.Fatalf("Error: could not setup operation: %s\n", err)
}
// Exit if operation contains no actions
if len(operation.Actions) == 0 {
fmt.Println("No action needs to be taken")
return
}
// Show operation summary
operation.ShowOperationSummary()
// Confirmation Prompt
if !yesAll {
reader := bufio.NewReader(os.Stdin)
if len(operation.Actions) == 1 {
fmt.Printf("Do you wish to install this package? [y\\N] ")
} else {
fmt.Printf("Do you wish to install these %d packages? [y\\N] ", len(operation.Actions))
}
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Println("Cancelling package installation...")
os.Exit(1)
}
}
// Execute operation
err = operation.Execute(verbose, force)
if err != nil {
log.Fatalf("Error: could not complete operation: %s\n", err)
}
// Executing hooks
fmt.Println("Running hooks...")
err = operation.RunHooks(verbose)
if err != nil {
log.Fatalf("Error: could not run hooks: %s\n", err)
}
case update:
// Check for required permissions
if os.Getuid() != 0 {
log.Fatalf("Error: this subcommand needs to be run with superuser permissions")
}
// Read local databases if no sync
if nosync {
err := bpmlib.ReadLocalDatabaseFiles()
if err != nil {
log.Fatalf("Error: could not read local databases: %s", err)
}
}
// Create update operation
operation, err := bpmlib.UpdatePackages(rootDir, !nosync, !noOptional, force, verbose)
if errors.As(err, &bpmlib.PackageNotFoundErr{}) || errors.As(err, &bpmlib.DependencyNotFoundErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) {
log.Fatalf("Error: %s", err)
} else if err != nil {
log.Fatalf("Error: could not setup operation: %s\n", err)
}
// Exit if operation contains no actions
if len(operation.Actions) == 0 {
fmt.Println("No action needs to be taken")
return
}
// Show operation summary
operation.ShowOperationSummary()
// Confirmation Prompt
if !yesAll {
fmt.Printf("Are you sure you wish to update all %d packages? [y\\N] ", len(operation.Actions))
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Println("Cancelling package update...")
os.Exit(1)
}
}
// Execute operation
err = operation.Execute(verbose, force)
if err != nil {
log.Fatalf("Error: could not complete operation: %s\n", err)
}
// Executing hooks
fmt.Println("Running hooks...")
err = operation.RunHooks(verbose)
if err != nil {
log.Fatalf("Error: could not run hooks: %s\n", err)
}
case sync:
// Check for required permissions
if os.Getuid() != 0 {
log.Fatalf("Error: this subcommand needs to be run with superuser permissions")
}
// Confirmation Prompt
if !yesAll {
fmt.Printf("Are you sure you wish to sync all databases? [y\\N] ")
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Println("Cancelling database synchronization...")
os.Exit(1)
}
}
// Sync databases
err := bpmlib.SyncDatabase(verbose)
if err != nil {
log.Fatalf("Error: could not sync local database: %s\n", err)
}
fmt.Println("All package databases synced successfully!")
case remove:
// Check for required permissions
if os.Getuid() != 0 {
log.Fatalf("Error: this subcommand needs to be run with superuser permissions")
}
if len(subcommandArgs) == 0 {
fmt.Println("No packages were given")
return
}
// Read local databases
err := bpmlib.ReadLocalDatabaseFiles()
if err != nil {
log.Fatalf("Error: could not read local databases: %s", err)
}
// Create remove operation
operation, err := bpmlib.RemovePackages(rootDir, removeUnused, doCleanup, subcommandArgs...)
if errors.As(err, &bpmlib.PackageNotFoundErr{}) || errors.As(err, &bpmlib.DependencyNotFoundErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) {
log.Fatalf("Error: %s", err)
} else if err != nil {
log.Fatalf("Error: could not setup operation: %s\n", err)
}
// Exit if operation contains no actions
if len(operation.Actions) == 0 {
fmt.Println("No action needs to be taken")
return
}
// Show operation summary
operation.ShowOperationSummary()
// Confirmation Prompt
if !yesAll {
fmt.Printf("Are you sure you wish to remove all %d packages? [y\\N] ", len(operation.Actions))
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Println("Cancelling package removal...")
os.Exit(1)
}
}
// Execute operation
err = operation.Execute(verbose, force)
if err != nil {
log.Fatalf("Error: could not complete operation: %s\n", err)
}
// Executing hooks
fmt.Println("Running hooks...")
err = operation.RunHooks(verbose)
if err != nil {
log.Fatalf("Error: could not run hooks: %s\n", err)
}
case cleanup:
// Check for required permissions
if os.Getuid() != 0 {
log.Fatalf("Error: this subcommand needs to be run with superuser permissions")
}
err := bpmlib.CleanupCache(rootDir, cleanupCompilationFiles, cleanupCompiledPackages, cleanupFetchedPackages, verbose)
if err != nil {
log.Fatalf("Error: could not complete cache cleanup: %s", err)
}
if cleanupDependencies || cleanupMakeDependencies {
// Read local databases
err := bpmlib.ReadLocalDatabaseFiles()
if err != nil {
log.Fatalf("Error: could not read local databases: %s", err)
}
// Create cleanup operation
operation, err := bpmlib.CleanupPackages(cleanupMakeDependencies, rootDir)
if errors.As(err, &bpmlib.PackageNotFoundErr{}) || errors.As(err, &bpmlib.DependencyNotFoundErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) {
log.Fatalf("Error: %s", err)
} else if err != nil {
log.Fatalf("Error: could not setup operation: %s\n", err)
}
// Exit if operation contains no actions
if len(operation.Actions) == 0 {
fmt.Println("No action needs to be taken")
return
}
// Show operation summary
operation.ShowOperationSummary()
// Confirmation Prompt
if !yesAll {
fmt.Printf("Are you sure you wish to remove all %d packages? [y\\N] ", len(operation.Actions))
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Println("Cancelling package removal...")
os.Exit(1)
}
}
// Execute operation
err = operation.Execute(verbose, force)
if err != nil {
log.Fatalf("Error: could not complete operation: %s\n", err)
}
// Executing hooks
fmt.Println("Running hooks...")
err = operation.RunHooks(verbose)
if err != nil {
log.Fatalf("Error: could not run hooks: %s\n", err)
}
}
case file:
files := subcommandArgs
if len(files) == 0 {
fmt.Println("No files were given to get which packages manage it")
return
}
for _, file := range files {
absFile, err := filepath.Abs(file)
if err != nil {
log.Fatalf("Error: could not get absolute path of file (%s)\n", file)
}
stat, err := os.Stat(absFile)
if os.IsNotExist(err) {
log.Fatalf("Error: file (%s) does not exist!\n", absFile)
}
pkgs, err := bpmlib.GetInstalledPackages(rootDir)
if err != nil {
log.Fatalf("Error: could not get installed packages: %s\n", err.Error())
}
if !strings.HasPrefix(absFile, rootDir) {
log.Fatalf("Error: could not get path of file (%s) relative to root path", absFile)
}
absFile, err = filepath.Rel(rootDir, absFile)
if err != nil {
log.Fatalf("Error: could not get path of file (%s) relative to root path", absFile)
}
absFile = strings.TrimPrefix(absFile, "/")
if stat.IsDir() {
absFile = absFile + "/"
}
var pkgList []string
for _, pkg := range pkgs {
if slices.ContainsFunc(bpmlib.GetPackage(pkg, rootDir).PkgFiles, func(entry *bpmlib.PackageFileEntry) bool {
return entry.Path == absFile
}) {
pkgList = append(pkgList, pkg)
}
}
if len(pkgList) == 0 {
fmt.Println(absFile + " is not managed by any packages")
} else {
fmt.Println(absFile + " is managed by the following packages:")
for _, pkg := range pkgList {
fmt.Println("- " + pkg)
}
}
}
case compile:
if len(subcommandArgs) == 0 {
fmt.Println("No source packages were given")
return
}
// Read local databases
err := bpmlib.ReadLocalDatabaseFiles()
if err != nil {
log.Fatalf("Error: could not read local databases: %s", err)
}
// Compile packages
for _, sourcePackage := range subcommandArgs {
if _, err := os.Stat(sourcePackage); os.IsNotExist(err) {
log.Fatalf("Error: file (%s) does not exist!", sourcePackage)
}
// Read archive
bpmpkg, err := bpmlib.ReadPackage(sourcePackage)
if err != nil {
log.Fatalf("Could not read package (%s): %s", sourcePackage, err)
}
// Ensure archive is source BPM package
if bpmpkg.PkgInfo.Type != "source" {
log.Fatalf("Error: cannot compile a non-source package!")
}
// Get direct runtime and make dependencies
totalDepends := make([]string, 0)
for depend := range bpmpkg.PkgInfo.GetDependencies(true, false) {
if !slices.Contains(totalDepends, depend) {
totalDepends = append(totalDepends, depend)
}
}
// 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()
}
}
func printHelp() {
fmt.Println("\033[1m---- Command Format ----\033[0m")
fmt.Println("-> command format: bpm <subcommand> [-flags]...")
fmt.Println("-> flags will be read if passed right after the subcommand otherwise they will be read as subcommand arguments")
fmt.Println("\033[1m---- Command List ----\033[0m")
fmt.Println("-> bpm version | shows information on the installed version of bpm")
fmt.Println("-> bpm info [-R, --databases] <packages...> | shows information on an installed package")
fmt.Println(" -R=<path> lets you define the root path which will be used")
fmt.Println(" --databases show information on package in configured databases")
fmt.Println("-> bpm list [-R, -c, -n] | lists all installed packages")
fmt.Println(" -R=<path> 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 <search terms...> | Searches for packages through configured databases")
fmt.Println("-> bpm install [-R, -v, -y, -f, --reinstall, --reinstall-all, --no-optional, --installation-reason] <packages...> | installs the following files")
fmt.Println(" -R=<path> lets you define the root path which will be used")
fmt.Println(" -v Show additional information about what BPM is doing")
fmt.Println(" -y skips the confirmation prompt")
fmt.Println(" -f skips dependency, conflict and architecture checking")
fmt.Println(" --reinstall Reinstalls packages even if they do not have a newer version available")
fmt.Println(" --reinstall-all Same as --reinstall but also reinstalls dependencies")
fmt.Println(" --no-optional Prevents installation of optional dependencies")
fmt.Println(" --installation-reason=<manual/dependency> sets the installation reason for all newly installed packages")
fmt.Println("-> bpm update [-R, -v, -y, -f, --reinstall, --no-sync] | updates all packages that are available in the configured databases")
fmt.Println(" -R=<path> lets you define the root path which will be used")
fmt.Println(" -v Show additional information about what BPM is doing")
fmt.Println(" -y skips the confirmation prompt")
fmt.Println(" -f skips dependency, conflict and architecture checking")
fmt.Println(" --reinstall Fetches and reinstalls all packages even if they do not have a newer version available")
fmt.Println(" --no-sync Skips package database syncing")
fmt.Println("-> bpm sync [-R, -v, -y] | Syncs package databases without updating packages")
fmt.Println(" -R=<path> lets you define the root path which will be used")
fmt.Println(" -v Show additional information about what BPM is doing")
fmt.Println(" -y skips the confirmation prompt")
fmt.Println("-> bpm remove [-R, -v, -y, --unused, --cleanup] <packages...> | removes the following packages")
fmt.Println(" -v Show additional information about what BPM is doing")
fmt.Println(" -R=<path> lets you define the root path which will be used")
fmt.Println(" -y skips the confirmation prompt")
fmt.Println(" --unused removes only packages that aren't required as dependencies by other packages")
fmt.Println(" --cleanup performs a dependency cleanup")
fmt.Println("-> bpm cleanup [-R, -v, -y, --depends, --compilation-files, --compiled-pkgs, --fetched-pkgs] | remove all unused dependencies and cache directories")
fmt.Println(" -v Show additional information about what BPM is doing")
fmt.Println(" -R=<path> 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] <files...> | shows what packages the following packages are managed by")
fmt.Println(" -R=<root_path> lets you define the root path which will be used")
fmt.Println("-> bpm compile [-d, -s, -o] <source packages...> | Compile source BPM package")
fmt.Println(" -v Show additional information about what BPM is doing")
fmt.Println(" -d installs required dependencies for package compilation")
fmt.Println(" -s skips the check function in source.sh scripts")
fmt.Println(" -o sets output directory")
fmt.Println(" -y skips the confirmation prompt")
fmt.Println("\033[1m----------------\033[0m")
}
func resolveFlags() {
// List flags
listFlagSet := flag.NewFlagSet("List flags", flag.ExitOnError)
listFlagSet.Usage = printHelp
listFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
listFlagSet.BoolVar(&pkgListNumbers, "c", false, "List the number of all packages installed with BPM")
listFlagSet.BoolVar(&pkgListNames, "n", false, "List the names of all packages installed with BPM")
// Info flags
infoFlagSet := flag.NewFlagSet("Info flags", flag.ExitOnError)
infoFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
infoFlagSet.BoolVar(&showDatabaseInfo, "databases", false, "Show information on package in configured databases")
infoFlagSet.Usage = printHelp
// Install flags
installFlagSet := flag.NewFlagSet("Install flags", flag.ExitOnError)
installFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
installFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing")
installFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts")
installFlagSet.BoolVar(&force, "f", false, "Force installation by skipping architecture and dependency resolution")
installFlagSet.BoolVar(&reinstall, "reinstall", false, "Reinstalls packages even if they do not have a newer version available")
installFlagSet.BoolVar(&reinstallAll, "reinstall-all", false, "Same as --reinstall but also reinstalls dependencies")
installFlagSet.BoolVar(&noOptional, "no-optional", false, "Prevents installation of optional dependencies")
installFlagSet.StringVar(&installationReason, "installation-reason", "", "Set the installation reason for all newly installed packages")
installFlagSet.Usage = printHelp
// Update flags
updateFlagSet := flag.NewFlagSet("Update flags", flag.ExitOnError)
updateFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
updateFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing")
updateFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts")
updateFlagSet.BoolVar(&force, "f", false, "Force update by skipping architecture and dependency resolution")
updateFlagSet.BoolVar(&nosync, "no-sync", false, "Skips package database syncing")
updateFlagSet.Usage = printHelp
// Sync flags
syncFlagSet := flag.NewFlagSet("Sync flags", flag.ExitOnError)
syncFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
syncFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing")
syncFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts")
syncFlagSet.Usage = printHelp
// Remove flags
removeFlagSet := flag.NewFlagSet("Remove flags", flag.ExitOnError)
removeFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
removeFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing")
removeFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts")
removeFlagSet.BoolVar(&removeUnused, "unused", false, "Removes only packages that aren't required as dependencies by other packages")
removeFlagSet.BoolVar(&doCleanup, "cleanup", false, "Perform a dependency cleanup")
removeFlagSet.Usage = printHelp
// Cleanup flags
cleanupFlagSet := flag.NewFlagSet("Cleanup flags", flag.ExitOnError)
cleanupFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
cleanupFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing")
cleanupFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts")
cleanupFlagSet.BoolVar(&cleanupDependencies, "depends", false, "Perform a dependency cleanup")
cleanupFlagSet.BoolVar(&cleanupMakeDependencies, "make-depends", false, "Perform a make dependency cleanup")
cleanupFlagSet.BoolVar(&cleanupCompilationFiles, "compilation-files", false, "Perform a cleanup of compilation files")
cleanupFlagSet.BoolVar(&cleanupCompiledPackages, "compiled-pkgs", false, "Perform a cleanup of compilation compiled binary packages")
cleanupFlagSet.BoolVar(&cleanupFetchedPackages, "fetched-pkgs", false, "Perform a cleanup of fetched packages from databases")
cleanupFlagSet.Usage = printHelp
// File flags
fileFlagSet := flag.NewFlagSet("Remove flags", flag.ExitOnError)
fileFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
fileFlagSet.Usage = printHelp
// Compile flags
compileFlagSet := flag.NewFlagSet("Compile flags", flag.ExitOnError)
compileFlagSet.BoolVar(&installSrcPkgDepends, "d", false, "Install required dependencies for package compilation")
compileFlagSet.BoolVar(&skipChecks, "s", false, "Skip the check function in source.sh scripts")
compileFlagSet.StringVar(&outputDirectory, "o", "", "Set output directory")
compileFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing")
compileFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts")
compileFlagSet.Usage = printHelp
isFlagSet := func(flagSet *flag.FlagSet, name string) bool {
found := false
flagSet.Visit(func(f *flag.Flag) {
if f.Name == name {
found = true
}
})
return found
}
if len(os.Args[1:]) <= 0 {
subcommand = "help"
} else {
subcommand = os.Args[1]
subcommandArgs = os.Args[2:]
if getCommandType() == list {
err := listFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = listFlagSet.Args()
} else if getCommandType() == info {
err := infoFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = infoFlagSet.Args()
} else if getCommandType() == install {
err := installFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = installFlagSet.Args()
} else if getCommandType() == update {
err := updateFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = updateFlagSet.Args()
} else if getCommandType() == sync {
err := syncFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = syncFlagSet.Args()
} else if getCommandType() == remove {
err := removeFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = removeFlagSet.Args()
} else if getCommandType() == cleanup {
err := cleanupFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
if !isFlagSet(cleanupFlagSet, "depends") && !isFlagSet(cleanupFlagSet, "make-depends") && !isFlagSet(cleanupFlagSet, "compilation-files") && !isFlagSet(cleanupFlagSet, "compiled-pkgs") && !isFlagSet(cleanupFlagSet, "fetched-pkgs") {
cleanupDependencies = true
cleanupMakeDependencies = bpmlib.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
}
}
}

382
src/bpmlib/compilation.go Normal file
View File

@ -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
}

43
src/bpmlib/config.go Normal file
View File

@ -0,0 +1,43 @@
package bpmlib
import (
"gopkg.in/yaml.v3"
"os"
)
type BPMConfigStruct struct {
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
func ReadConfig() (err error) {
if _, err = os.Stat("/etc/bpm.conf"); os.IsNotExist(err) {
return err
}
bytes, err := os.ReadFile("/etc/bpm.conf")
if err != nil {
return err
}
BPMConfig = BPMConfigStruct{
CleanupMakeDependencies: true,
}
err = yaml.Unmarshal(bytes, &BPMConfig)
if err != nil {
return err
}
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:]...)
}
}
return nil
}

281
src/bpmlib/databases.go Normal file
View File

@ -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
}

173
src/bpmlib/dependencies.go Normal file
View File

@ -0,0 +1,173 @@
package bpmlib
import (
"errors"
"fmt"
"slices"
)
func (pkgInfo *PackageInfo) GetDependencies(includeMakeDepends, includeOptionalDepends bool) map[string]InstallationReason {
allDepends := make(map[string]InstallationReason)
for _, depend := range pkgInfo.Depends {
allDepends[depend] = InstallationReasonDependency
}
if includeOptionalDepends {
for _, depend := range pkgInfo.OptionalDepends {
if _, ok := allDepends[depend]; !ok {
allDepends[depend] = InstallationReasonDependency
}
}
}
if includeMakeDepends {
for _, depend := range pkgInfo.MakeDepends {
if _, ok := allDepends[depend]; !ok {
allDepends[depend] = 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 depend := range pkgInfo.GetDependencies(includeMakeDepends, includeOptionalDepends) {
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 map[string]InstallationReason, unresolved []string) {
// Initialize slices
resolved = make(map[string]InstallationReason)
unresolved = make([]string, 0)
// Call unexported function
resolvePackageDependenciesFromDatabase(resolved, &unresolved, pkgInfo, InstallationReasonDependency, checkMake, checkOptional, ignoreInstalled, verbose, rootDir)
return resolved, unresolved
}
func resolvePackageDependenciesFromDatabase(resolved map[string]InstallationReason, unresolved *[]string, pkgInfo *PackageInfo, installationReason InstallationReason, checkMake, checkOptional, ignoreInstalled, verbose bool, rootDir string) {
// Add current package name to unresolved slice
*unresolved = append(*unresolved, pkgInfo.Name)
// Loop through all dependencies
for depend, ir := range pkgInfo.GetDependencies(pkgInfo.Type == "source", checkOptional) {
if _, ok := resolved[depend]; !ok {
// Add current dependency to resolved slice when circular dependency is detected
if slices.Contains(*unresolved, depend) {
if verbose {
fmt.Printf("Circular dependency was detected (%s -> %s). Installing %s first\n", pkgInfo.Name, depend, depend)
}
if _, ok := resolved[depend]; !ok {
resolved[depend] = ir
}
continue
} else if ignoreInstalled && IsPackageProvided(depend, rootDir) {
continue
}
var err error
var entry *BPMDatabaseEntry
entry, _, err = GetDatabaseEntry(depend)
if err != nil {
if entry = ResolveVirtualPackage(depend); entry == nil {
if !slices.Contains(*unresolved, depend) {
*unresolved = append(*unresolved, depend)
}
continue
}
}
resolvePackageDependenciesFromDatabase(resolved, unresolved, entry.Info, ir, checkMake, checkOptional, ignoreInstalled, verbose, rootDir)
}
}
if _, ok := resolved[pkgInfo.Name]; !ok {
resolved[pkgInfo.Name] = installationReason
}
*unresolved = stringSliceRemove(*unresolved, pkgInfo.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 _, ok := dependencies[pkgName]; ok {
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 _, ok := dependencies[vpkg]; ok {
ret = append(ret, installedPkgName)
break
}
}
}
return ret, nil
}

31
src/bpmlib/errors.go Normal file
View File

@ -0,0 +1,31 @@
package bpmlib
import (
"fmt"
"strings"
)
type PackageNotFoundErr struct {
packages []string
}
func (e PackageNotFoundErr) Error() string {
return "The following packages were not found in any databases: " + strings.Join(e.packages, ", ")
}
type DependencyNotFoundErr struct {
dependencies []string
}
func (e DependencyNotFoundErr) Error() string {
return "The following dependencies were not found in any databases: " + strings.Join(e.dependencies, ", ")
}
type PackageConflictErr struct {
pkg string
conflicts []string
}
func (e PackageConflictErr) Error() string {
return fmt.Sprintf("Package (%s) is in conflict with the following packages: %s", e.pkg, strings.Join(e.conflicts, ", "))
}

368
src/bpmlib/general.go Normal file
View File

@ -0,0 +1,368 @@
package bpmlib
import (
"errors"
"fmt"
"log"
"os"
"path"
"slices"
)
type ReinstallMethod uint8
const (
ReinstallMethodNone ReinstallMethod = iota
ReinstallMethodSpecified ReinstallMethod = iota
ReinstallMethodAll ReinstallMethod = iota
)
// InstallPackages installs the specified packages into the given root directory by fetching them from databases or directly from local bpm archives
func InstallPackages(rootDir string, installationReason InstallationReason, reinstallMethod ReinstallMethod, installOptionalDependencies, forceInstallation, verbose bool, packages ...string) (operation *BPMOperation, err error) {
// Setup operation struct
operation = &BPMOperation{
Actions: make([]OperationAction, 0),
UnresolvedDepends: make([]string, 0),
Changes: make(map[string]string),
RootDir: rootDir,
compiledPackages: make(map[string]string),
}
// Resolve packages
pkgsNotFound := make([]string, 0)
for _, pkg := range packages {
if stat, err := os.Stat(pkg); err == nil && !stat.IsDir() {
bpmpkg, err := ReadPackage(pkg)
if err != nil {
return nil, fmt.Errorf("could not read package: %s", err)
}
if bpmpkg.PkgInfo.Type == "source" && bpmpkg.PkgInfo.IsSplitPackage() {
for _, splitPkg := range bpmpkg.PkgInfo.SplitPackages {
if reinstallMethod == ReinstallMethodNone && IsPackageInstalled(splitPkg.Name, rootDir) && GetPackageInfo(splitPkg.Name, rootDir).GetFullVersion() == splitPkg.GetFullVersion() {
continue
}
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
}
operation.AppendAction(&InstallPackageAction{
File: pkg,
InstallationReason: installationReason,
BpmPackage: bpmpkg,
})
} else {
var entry *BPMDatabaseEntry
if e, _, err := GetDatabaseEntry(pkg); err == nil {
entry = e
} else if isVirtual, p := IsVirtualPackage(pkg, rootDir); isVirtual {
entry, _, err = GetDatabaseEntry(p)
if err != nil {
pkgsNotFound = append(pkgsNotFound, pkg)
continue
}
} else if e := ResolveVirtualPackage(pkg); e != nil {
entry = e
} else {
pkgsNotFound = append(pkgsNotFound, pkg)
continue
}
if reinstallMethod == ReinstallMethodNone && IsPackageInstalled(entry.Info.Name, rootDir) && GetPackageInfo(entry.Info.Name, rootDir).GetFullVersion() == entry.Info.GetFullVersion() {
continue
}
operation.AppendAction(&FetchPackageAction{
InstallationReason: installationReason,
DatabaseEntry: entry,
})
}
}
// Return error if not all packages are found
if len(pkgsNotFound) != 0 {
return nil, PackageNotFoundErr{pkgsNotFound}
}
// Resolve dependencies
err = operation.ResolveDependencies(reinstallMethod == ReinstallMethodAll, installOptionalDependencies, verbose)
if err != nil {
return nil, fmt.Errorf("could not resolve dependencies: %s", err)
}
if len(operation.UnresolvedDepends) != 0 {
if !forceInstallation {
return nil, DependencyNotFoundErr{operation.UnresolvedDepends}
} else if verbose {
log.Printf("Warning: %s", DependencyNotFoundErr{operation.UnresolvedDepends})
}
}
// Replace obsolete packages
operation.ReplaceObsoletePackages()
// Check for conflicts
conflicts, err := operation.CheckForConflicts()
if err != nil {
return nil, fmt.Errorf("could not complete package conflict check: %s", err)
}
if len(conflicts) > 0 {
err = nil
for pkg, conflict := range conflicts {
err = errors.Join(err, PackageConflictErr{pkg, conflict})
}
if !forceInstallation {
return nil, err
} else {
log.Printf("Warning: %s", err)
}
}
return operation, nil
}
// RemovePackages removes the specified packages from the given root directory
func RemovePackages(rootDir string, removeUnusedPackagesOnly, cleanupDependencies bool, packages ...string) (operation *BPMOperation, err error) {
operation = &BPMOperation{
Actions: make([]OperationAction, 0),
UnresolvedDepends: make([]string, 0),
Changes: make(map[string]string),
RootDir: rootDir,
compiledPackages: make(map[string]string),
}
// Search for packages
for _, pkg := range packages {
bpmpkg := GetPackage(pkg, rootDir)
if bpmpkg == nil {
continue
}
operation.AppendAction(&RemovePackageAction{BpmPackage: bpmpkg})
}
// Do not remove packages which other packages depend on
if removeUnusedPackagesOnly {
err := operation.RemoveNeededPackages()
if err != nil {
return nil, fmt.Errorf("could not skip needed packages: %s", err)
}
}
// Do package cleanup
if cleanupDependencies {
err := operation.Cleanup(true)
if err != nil {
return nil, fmt.Errorf("could not perform cleanup for operation: %s", err)
}
}
return operation, nil
}
// CleanupPackages finds packages installed as dependencies which are no longer required by the rest of the system in the given root directory
func CleanupPackages(cleanupMakeDepends bool, rootDir string) (operation *BPMOperation, err error) {
operation = &BPMOperation{
Actions: make([]OperationAction, 0),
UnresolvedDepends: make([]string, 0),
Changes: make(map[string]string),
RootDir: rootDir,
compiledPackages: make(map[string]string),
}
// Do package cleanup
err = operation.Cleanup(cleanupMakeDepends)
if err != nil {
return nil, fmt.Errorf("could not perform cleanup for operation: %s", err)
}
return operation, nil
}
func CleanupCache(rootDir string, cleanupCompilationFiles, cleanupCompiledPackages, cleanupFetchedPackages, verbose bool) error {
if cleanupCompilationFiles {
globalCompilationCacheDir := path.Join(rootDir, "var/cache/bpm/compilation")
// Ensure path exists and is a directory
if stat, err := os.Stat(globalCompilationCacheDir); err == nil && stat.IsDir() {
// Delete directory
if verbose {
log.Printf("Removing directory (%s)\n", globalCompilationCacheDir)
}
err = os.RemoveAll(globalCompilationCacheDir)
if err != nil {
return err
}
}
// Get home directories of users in root directory
homeDirs, err := os.ReadDir(path.Join(rootDir, "home"))
if err != nil {
return err
}
// Loop through all home directories
for _, homeDir := range homeDirs {
// Skip if not a directory
if !homeDir.IsDir() {
continue
}
localCompilationDir := path.Join(rootDir, "home", homeDir.Name(), ".cache/bpm/compilation")
// Ensure path exists and is a directory
if stat, err := os.Stat(localCompilationDir); err != nil || !stat.IsDir() {
continue
}
// Delete directory
if verbose {
log.Printf("Removing directory (%s)\n", localCompilationDir)
}
err = os.RemoveAll(localCompilationDir)
if err != nil {
return err
}
}
}
if cleanupCompiledPackages {
dirToRemove := path.Join(rootDir, "var/cache/bpm/compiled")
// Ensure path exists and is a directory
if stat, err := os.Stat(dirToRemove); err == nil && stat.IsDir() {
// Delete directory
if verbose {
log.Printf("Removing directory (%s)\n", dirToRemove)
}
err = os.RemoveAll(dirToRemove)
if err != nil {
return err
}
}
}
if cleanupFetchedPackages {
dirToRemove := path.Join(rootDir, "var/cache/bpm/fetched")
// Ensure path exists and is a directory
if stat, err := os.Stat(dirToRemove); err == nil && stat.IsDir() {
// Delete directory
if verbose {
log.Printf("Removing directory (%s)\n", dirToRemove)
}
err = os.RemoveAll(dirToRemove)
if err != nil {
return err
}
}
}
return nil
}
// UpdatePackages fetches the newest versions of all installed packages from
func UpdatePackages(rootDir string, syncDatabase bool, installOptionalDependencies, forceInstallation, verbose bool) (operation *BPMOperation, err error) {
// Sync databases
if syncDatabase {
err := SyncDatabase(verbose)
if err != nil {
return nil, fmt.Errorf("could not sync local database: %s", err)
}
if verbose {
fmt.Println("All package databases synced successfully!")
}
// Reload config and local databases
err = ReadConfig()
if err != nil {
return nil, fmt.Errorf("could not read BPM config: %s", err)
}
err = ReadLocalDatabaseFiles()
if err != nil {
return nil, fmt.Errorf("could not read local databases: %s", err)
}
}
// Get installed packages and check for updates
pkgs, err := GetInstalledPackages(rootDir)
if err != nil {
return nil, fmt.Errorf("could not get installed packages: %s", err)
}
operation = &BPMOperation{
Actions: make([]OperationAction, 0),
UnresolvedDepends: make([]string, 0),
Changes: make(map[string]string),
RootDir: rootDir,
compiledPackages: make(map[string]string),
}
// Search for packages
for _, pkg := range pkgs {
if slices.Contains(BPMConfig.IgnorePackages, pkg) {
continue
}
var entry *BPMDatabaseEntry
// Check if installed package can be replaced and install that instead
if e := FindReplacement(pkg); e != nil {
entry = e
} else if entry, _, err = GetDatabaseEntry(pkg); err != nil {
continue
}
installedInfo := GetPackageInfo(pkg, rootDir)
if installedInfo == nil {
return nil, fmt.Errorf("could not get package info for package (%s)", pkg)
} else {
comparison := ComparePackageVersions(*entry.Info, *installedInfo)
if comparison > 0 {
operation.AppendAction(&FetchPackageAction{
InstallationReason: GetInstallationReason(pkg, rootDir),
DatabaseEntry: entry,
})
}
}
}
// Check for new dependencies in updated packages
err = operation.ResolveDependencies(false, installOptionalDependencies, verbose)
if err != nil {
return nil, fmt.Errorf("could not resolve dependencies: %s", err)
}
if len(operation.UnresolvedDepends) != 0 {
if !forceInstallation {
return nil, DependencyNotFoundErr{operation.UnresolvedDepends}
} else if verbose {
log.Printf("Warning: %s", DependencyNotFoundErr{operation.UnresolvedDepends})
}
}
// Replace obsolete packages
operation.ReplaceObsoletePackages()
return operation, nil
}
// SyncDatabase syncs all databases declared in /etc/bpm.conf
func SyncDatabase(verbose bool) (err error) {
for _, db := range BPMConfig.Databases {
if verbose {
fmt.Printf("Fetching package database file for database (%s)...\n", db.Name)
}
err := db.SyncLocalDatabaseFile()
if err != nil {
return err
}
}
return nil
}

8
src/bpmlib/go.mod Normal file
View File

@ -0,0 +1,8 @@
module git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib
go 1.23
require (
github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f
gopkg.in/yaml.v3 v3.0.1
)

6
src/bpmlib/go.sum Normal file
View File

@ -0,0 +1,6 @@
github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f h1:xt29M2T6STgldg+WEP51gGePQCsQvklmP2eIhPIBK3g=
github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

158
src/bpmlib/hooks.go Normal file
View File

@ -0,0 +1,158 @@
package bpmlib
import (
"errors"
"fmt"
"gopkg.in/yaml.v3"
"os"
"os/exec"
"path"
"path/filepath"
"slices"
"strings"
"syscall"
)
type BPMHook struct {
SourcePath string
SourceContent string
TriggerOperations []string `yaml:"trigger_operations"`
TargetType string `yaml:"target_type"`
Targets []string `yaml:"targets"`
Depends []string `yaml:"depends"`
Run string `yaml:"run"`
}
// createHook returns a BPMHook instance based on the content of the given string
func createHook(sourcePath string) (*BPMHook, error) {
// Read hook from source path
bytes, err := os.ReadFile(sourcePath)
if err != nil {
return nil, err
}
// Create base hook structure
hook := &BPMHook{
SourcePath: sourcePath,
SourceContent: string(bytes),
TriggerOperations: nil,
TargetType: "",
Targets: nil,
Depends: nil,
Run: "",
}
// Unmarshal yaml string
err = yaml.Unmarshal(bytes, hook)
if err != nil {
return nil, err
}
// Ensure hook is valid
if err := hook.IsValid(); err != nil {
return nil, err
}
return hook, nil
}
// IsValid ensures hook is valid
func (hook *BPMHook) IsValid() error {
ValidOperations := []string{"install", "upgrade", "remove"}
// Return error if any trigger operation is not valid or none are given
if len(hook.TriggerOperations) == 0 {
return errors.New("no trigger operations specified")
}
for _, operation := range hook.TriggerOperations {
if !slices.Contains(ValidOperations, operation) {
return errors.New("trigger operation '" + operation + "' is not valid")
}
}
if hook.TargetType != "package" && hook.TargetType != "path" {
return errors.New("target type '" + hook.TargetType + "' is not valid")
}
if len(hook.Run) == 0 {
return errors.New("command to run is empty")
}
// Return nil as hook is valid
return nil
}
// Execute hook if all conditions are met
func (hook *BPMHook) Execute(packageChanges map[string]string, verbose bool, rootDir string) error {
// Check if package dependencies are met
installedPackages, err := GetInstalledPackages(rootDir)
if err != nil {
return err
}
for _, depend := range hook.Depends {
if !slices.Contains(installedPackages, depend) {
return nil
}
}
// Get modified files slice
modifiedFiles := make([]*PackageFileEntry, 0)
for pkg := range packageChanges {
if GetPackage(pkg, rootDir) != nil {
modifiedFiles = append(modifiedFiles, GetPackage(pkg, rootDir).PkgFiles...)
}
}
// Check if any targets are met
targetMet := false
for _, target := range hook.Targets {
if targetMet {
break
}
if hook.TargetType == "package" {
for change, operation := range packageChanges {
if target == change && slices.Contains(hook.TriggerOperations, operation) {
targetMet = true
break
}
}
} else {
glob, err := filepath.Glob(path.Join(rootDir, target))
if err != nil {
return err
}
for _, change := range modifiedFiles {
if slices.Contains(glob, path.Join(rootDir, change.Path)) {
targetMet = true
break
}
}
}
}
if !targetMet {
return nil
}
// Execute the command
splitCommand := strings.Split(hook.Run, " ")
cmd := exec.Command(splitCommand[0], splitCommand[1:]...)
// Setup subprocess environment
cmd.Dir = "/"
// Run hook in chroot if using the -R flag
if rootDir != "/" {
cmd.SysProcAttr = &syscall.SysProcAttr{Chroot: rootDir}
}
if verbose {
fmt.Printf("Running hook (%s) with run command: %s\n", hook.SourcePath, strings.Join(splitCommand, " "))
}
err = cmd.Run()
if err != nil {
return err
}
return nil
}

View File

@ -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
}

621
src/bpmlib/operations.go Normal file
View File

@ -0,0 +1,621 @@
package bpmlib
import (
"errors"
"fmt"
"log"
"os"
"path"
"slices"
"strings"
)
type BPMOperation struct {
Actions []OperationAction
UnresolvedDepends []string
Changes map[string]string
RootDir string
compiledPackages map[string]string
}
func (operation *BPMOperation) ActionsContainPackage(pkg string) bool {
for _, action := range operation.Actions {
if action.GetActionType() == "install" {
if action.(*InstallPackageAction).BpmPackage.PkgInfo.Name == pkg {
return true
}
} else if action.GetActionType() == "fetch" {
if action.(*FetchPackageAction).DatabaseEntry.Info.Name == pkg {
return true
}
} else if action.GetActionType() == "remove" {
if action.(*RemovePackageAction).BpmPackage.PkgInfo.Name == pkg {
return true
}
}
}
return false
}
func (operation *BPMOperation) AppendAction(action OperationAction) {
operation.InsertActionAt(len(operation.Actions), action)
}
func (operation *BPMOperation) InsertActionAt(index int, action OperationAction) {
if len(operation.Actions) == index { // nil or empty slice or after last element
operation.Actions = append(operation.Actions, action)
} else {
operation.Actions = append(operation.Actions[:index+1], operation.Actions[index:]...) // index < len(a)
operation.Actions[index] = action
}
if action.GetActionType() == "install" {
pkgInfo := action.(*InstallPackageAction).BpmPackage.PkgInfo
if !IsPackageInstalled(pkgInfo.Name, operation.RootDir) {
operation.Changes[pkgInfo.Name] = "install"
} else {
operation.Changes[pkgInfo.Name] = "upgrade"
}
} else if action.GetActionType() == "fetch" {
pkgInfo := action.(*FetchPackageAction).DatabaseEntry.Info
if !IsPackageInstalled(pkgInfo.Name, operation.RootDir) {
operation.Changes[pkgInfo.Name] = "install"
} else {
operation.Changes[pkgInfo.Name] = "upgrade"
}
} else if action.GetActionType() == "remove" {
operation.Changes[action.(*RemovePackageAction).BpmPackage.PkgInfo.Name] = "remove"
}
}
func (operation *BPMOperation) RemoveAction(pkg, actionType string) {
operation.Actions = slices.DeleteFunc(operation.Actions, func(a OperationAction) bool {
if a.GetActionType() != actionType {
return false
}
if a.GetActionType() == "install" {
return a.(*InstallPackageAction).BpmPackage.PkgInfo.Name == pkg
} else if a.GetActionType() == "fetch" {
return a.(*FetchPackageAction).DatabaseEntry.Info.Name == pkg
} else if a.GetActionType() == "remove" {
return a.(*RemovePackageAction).BpmPackage.PkgInfo.Name == pkg
}
return false
})
}
func (operation *BPMOperation) GetTotalDownloadSize() uint64 {
var ret uint64 = 0
for _, action := range operation.Actions {
if action.GetActionType() == "fetch" {
ret += action.(*FetchPackageAction).DatabaseEntry.DownloadSize
}
}
return ret
}
func (operation *BPMOperation) GetTotalInstalledSize() uint64 {
var ret uint64 = 0
for _, action := range operation.Actions {
if action.GetActionType() == "install" {
ret += action.(*InstallPackageAction).BpmPackage.GetInstalledSize()
} else if action.GetActionType() == "fetch" {
ret += action.(*FetchPackageAction).DatabaseEntry.InstalledSize
}
}
return ret
}
func (operation *BPMOperation) GetFinalActionSize(rootDir string) int64 {
var ret int64 = 0
for _, action := range operation.Actions {
if action.GetActionType() == "install" {
ret += int64(action.(*InstallPackageAction).BpmPackage.GetInstalledSize())
if IsPackageInstalled(action.(*InstallPackageAction).BpmPackage.PkgInfo.Name, rootDir) {
ret -= int64(GetPackage(action.(*InstallPackageAction).BpmPackage.PkgInfo.Name, rootDir).GetInstalledSize())
}
} else if action.GetActionType() == "fetch" {
ret += int64(action.(*FetchPackageAction).DatabaseEntry.InstalledSize)
} else if action.GetActionType() == "remove" {
ret -= int64(action.(*RemovePackageAction).BpmPackage.GetInstalledSize())
}
}
return ret
}
func (operation *BPMOperation) ResolveDependencies(reinstallDependencies, installOptionalDependencies, verbose bool) error {
pos := 0
for _, value := range slices.Clone(operation.Actions) {
var pkgInfo *PackageInfo
if value.GetActionType() == "install" {
action := value.(*InstallPackageAction)
pkgInfo = action.BpmPackage.PkgInfo
} else if value.GetActionType() == "fetch" {
action := value.(*FetchPackageAction)
pkgInfo = action.DatabaseEntry.Info
} else {
pos++
continue
}
resolved, unresolved := ResolveAllPackageDependenciesFromDatabases(pkgInfo, pkgInfo.Type == "source", installOptionalDependencies, !reinstallDependencies, verbose, operation.RootDir)
operation.UnresolvedDepends = append(operation.UnresolvedDepends, unresolved...)
for depend, installationReason := range resolved {
if !operation.ActionsContainPackage(depend) && depend != pkgInfo.Name {
if !reinstallDependencies && IsPackageInstalled(depend, operation.RootDir) {
continue
}
entry, _, err := GetDatabaseEntry(depend)
if err != nil {
return errors.New("could not get database entry for package (" + depend + ")")
}
operation.InsertActionAt(pos, &FetchPackageAction{
InstallationReason: installationReason,
DatabaseEntry: entry,
})
pos++
}
}
pos++
}
return nil
}
func (operation *BPMOperation) RemoveNeededPackages() error {
removeActions := make(map[string]*RemovePackageAction)
for _, action := range slices.Clone(operation.Actions) {
if action.GetActionType() == "remove" {
removeActions[action.(*RemovePackageAction).BpmPackage.PkgInfo.Name] = action.(*RemovePackageAction)
}
}
for pkg, action := range removeActions {
dependants, err := GetPackageDependants(action.BpmPackage.PkgInfo.Name, operation.RootDir)
if err != nil {
return errors.New("could not get dependant packages for package (" + pkg + ")")
}
dependants = slices.DeleteFunc(dependants, func(d string) bool {
if _, ok := removeActions[d]; ok {
return true
}
return false
})
if len(dependants) != 0 {
operation.RemoveAction(pkg, action.GetActionType())
}
}
return nil
}
func (operation *BPMOperation) Cleanup(cleanupMakeDepends bool) error {
// Get all installed packages
installedPackageNames, err := GetInstalledPackages(operation.RootDir)
if err != nil {
return fmt.Errorf("could not get installed packages: %s", err)
}
installedPackages := make([]*PackageInfo, len(installedPackageNames))
for i, value := range installedPackageNames {
bpmpkg := GetPackage(value, operation.RootDir)
if bpmpkg == nil {
return errors.New("could not find installed package (" + value + ")")
}
installedPackages[i] = bpmpkg.PkgInfo
}
// Get packages to remove
removeActions := make(map[string]*RemovePackageAction)
for _, action := range slices.Clone(operation.Actions) {
if action.GetActionType() == "remove" {
removeActions[action.(*RemovePackageAction).BpmPackage.PkgInfo.Name] = action.(*RemovePackageAction)
}
}
// Get manually installed packages, resolve all their dependencies and add them to the keepPackages slice
keepPackages := make([]string, 0)
for _, pkg := range slices.Clone(installedPackages) {
if GetInstallationReason(pkg.Name, operation.RootDir) != InstallationReasonManual {
continue
}
// Do not resolve dependencies or add package to keepPackages slice if package removal action exists for it
if _, ok := removeActions[pkg.Name]; ok {
continue
}
keepPackages = append(keepPackages, pkg.Name)
resolved := pkg.GetAllDependencies(!cleanupMakeDepends, true, operation.RootDir)
for _, value := range resolved {
if !slices.Contains(keepPackages, value) {
keepPackages = append(keepPackages, value)
}
}
}
// Get all installed packages that are not in the keepPackages slice and add them to the BPM operation
for _, pkg := range installedPackageNames {
// Do not add package removal action if there already is one
if _, ok := removeActions[pkg]; ok {
continue
}
if !slices.Contains(keepPackages, pkg) {
bpmpkg := GetPackage(pkg, operation.RootDir)
if bpmpkg == nil {
return errors.New("Error: could not find installed package (" + pkg + ")")
}
operation.Actions = append(operation.Actions, &RemovePackageAction{BpmPackage: bpmpkg})
}
}
return nil
}
func (operation *BPMOperation) ReplaceObsoletePackages() {
for _, value := range slices.Clone(operation.Actions) {
var pkgInfo *PackageInfo
if value.GetActionType() == "install" {
action := value.(*InstallPackageAction)
pkgInfo = action.BpmPackage.PkgInfo
} else if value.GetActionType() == "fetch" {
action := value.(*FetchPackageAction)
pkgInfo = action.DatabaseEntry.Info
} else {
continue
}
for _, r := range pkgInfo.Replaces {
if bpmpkg := GetPackage(r, operation.RootDir); bpmpkg != nil && !operation.ActionsContainPackage(bpmpkg.PkgInfo.Name) {
operation.InsertActionAt(0, &RemovePackageAction{
BpmPackage: bpmpkg,
})
}
}
}
}
func (operation *BPMOperation) CheckForConflicts() (map[string][]string, error) {
conflicts := make(map[string][]string)
installedPackages, err := GetInstalledPackages(operation.RootDir)
if err != nil {
return nil, err
}
allPackages := make([]*PackageInfo, len(installedPackages))
for i, value := range installedPackages {
bpmpkg := GetPackage(value, operation.RootDir)
if bpmpkg == nil {
return nil, errors.New(fmt.Sprintf("could not find installed package (%s)", value))
}
allPackages[i] = bpmpkg.PkgInfo
}
// Add all new packages to the allPackages slice
for _, value := range slices.Clone(operation.Actions) {
if value.GetActionType() == "install" {
action := value.(*InstallPackageAction)
pkgInfo := action.BpmPackage.PkgInfo
allPackages = append(allPackages, pkgInfo)
} else if value.GetActionType() == "fetch" {
action := value.(*FetchPackageAction)
pkgInfo := action.DatabaseEntry.Info
allPackages = append(allPackages, pkgInfo)
} else if value.GetActionType() == "remove" {
action := value.(*RemovePackageAction)
pkgInfo := action.BpmPackage.PkgInfo
for i := len(allPackages) - 1; i >= 0; i-- {
info := allPackages[i]
if info.Name == pkgInfo.Name {
allPackages = append(allPackages[:i], allPackages[i+1:]...)
}
}
}
}
for _, value := range allPackages {
for _, conflict := range value.Conflicts {
if slices.ContainsFunc(allPackages, func(info *PackageInfo) bool {
return info.Name == conflict
}) {
conflicts[value.Name] = append(conflicts[value.Name], conflict)
}
}
}
return conflicts, nil
}
func (operation *BPMOperation) ShowOperationSummary() {
if len(operation.Actions) == 0 {
fmt.Println("No action needs to be taken")
return
}
for _, value := range operation.Actions {
var pkgInfo *PackageInfo
var installationReason = InstallationReasonUnknown
if value.GetActionType() == "install" {
installationReason = value.(*InstallPackageAction).InstallationReason
pkgInfo = value.(*InstallPackageAction).BpmPackage.PkgInfo
if value.(*InstallPackageAction).SplitPackageToInstall != "" {
pkgInfo = pkgInfo.GetSplitPackageInfo(value.(*InstallPackageAction).SplitPackageToInstall)
}
} else if value.GetActionType() == "fetch" {
installationReason = value.(*FetchPackageAction).InstallationReason
pkgInfo = value.(*FetchPackageAction).DatabaseEntry.Info
} else {
pkgInfo = value.(*RemovePackageAction).BpmPackage.PkgInfo
fmt.Printf("%s: %s (Remove)\n", pkgInfo.Name, pkgInfo.GetFullVersion())
continue
}
installedInfo := GetPackageInfo(pkgInfo.Name, operation.RootDir)
additionalInfo := ""
switch installationReason {
case InstallationReasonManual:
additionalInfo = "(Manual)"
case InstallationReasonDependency:
additionalInfo = "(Dependency)"
case InstallationReasonMakeDependency:
additionalInfo = "(Make dependency)"
default:
additionalInfo = "(Unknown)"
}
if pkgInfo.Type == "source" {
additionalInfo += " (From Source)"
}
if installedInfo == nil {
fmt.Printf("%s: %s (Install) %s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), additionalInfo)
} else {
comparison := ComparePackageVersions(*pkgInfo, *installedInfo)
if comparison < 0 {
fmt.Printf("%s: %s -> %s (Downgrade) %s\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), additionalInfo)
} else if comparison > 0 {
fmt.Printf("%s: %s -> %s (Upgrade) %s\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), additionalInfo)
} else {
fmt.Printf("%s: %s (Reinstall) %s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), additionalInfo)
}
}
}
if operation.RootDir != "/" {
fmt.Println("Warning: Operating in " + operation.RootDir)
}
if operation.GetTotalDownloadSize() > 0 {
fmt.Printf("%s will be downloaded to complete this operation\n", unsignedBytesToHumanReadable(operation.GetTotalDownloadSize()))
}
if operation.GetFinalActionSize(operation.RootDir) > 0 {
fmt.Printf("A total of %s will be installed after the operation finishes\n", bytesToHumanReadable(operation.GetFinalActionSize(operation.RootDir)))
} else if operation.GetFinalActionSize(operation.RootDir) < 0 {
fmt.Printf("A total of %s will be freed after the operation finishes\n", strings.TrimPrefix(bytesToHumanReadable(operation.GetFinalActionSize(operation.RootDir)), "-"))
}
}
func (operation *BPMOperation) RunHooks(verbose bool) error {
// Return if hooks directory does not exist
if stat, err := os.Stat(path.Join(operation.RootDir, "var/lib/bpm/hooks")); err != nil || !stat.IsDir() {
return nil
}
// Get directory entries in hooks directory
dirEntries, err := os.ReadDir(path.Join(operation.RootDir, "var/lib/bpm/hooks"))
if err != nil {
return err
}
// Find all hooks, validate and execute them
for _, entry := range dirEntries {
if entry.Type().IsRegular() && strings.HasSuffix(entry.Name(), ".bpmhook") {
hook, err := createHook(path.Join(operation.RootDir, "var/lib/bpm/hooks", entry.Name()))
if err != nil {
log.Printf("Error while reading hook (%s): %s", entry.Name(), err)
}
err = hook.Execute(operation.Changes, verbose, operation.RootDir)
if err != nil {
log.Printf("Warning: could not execute hook (%s): %s\n", entry.Name(), err)
continue
}
}
}
return nil
}
func (operation *BPMOperation) Execute(verbose, force bool) (err error) {
// Fetch packages from databases
if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool {
return action.GetActionType() == "fetch"
}) {
fmt.Println("Fetching packages from available databases...")
// Create map for fetched packages
fetchedPackages := make(map[string]string)
for i, action := range operation.Actions {
if action.GetActionType() != "fetch" {
continue
}
// Get database entry
entry := action.(*FetchPackageAction).DatabaseEntry
// Create bpmpkg variable
var bpmpkg *BPMPackage
// Check if package has already been fetched from download link
if _, ok := fetchedPackages[entry.Download]; !ok {
// Fetch package from database
fetchedPackage, err := entry.Database.FetchPackage(entry.Info.Name)
if err != nil {
return errors.New(fmt.Sprintf("could not fetch package (%s): %s\n", entry.Info.Name, err))
}
// Read fetched package
bpmpkg, err = ReadPackage(fetchedPackage)
if err != nil {
return errors.New(fmt.Sprintf("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 errors.New(fmt.Sprintf("could not read package (%s): %s\n", entry.Info.Name, err))
}
fmt.Printf("Package (%s) was successfully fetched!\n", entry.Info.Name)
}
if bpmpkg.PkgInfo.IsSplitPackage() {
operation.Actions[i] = &InstallPackageAction{
File: fetchedPackages[entry.Download],
InstallationReason: action.(*FetchPackageAction).InstallationReason,
BpmPackage: bpmpkg,
SplitPackageToInstall: entry.Info.Name,
}
} else {
operation.Actions[i] = &InstallPackageAction{
File: fetchedPackages[entry.Download],
InstallationReason: action.(*FetchPackageAction).InstallationReason,
BpmPackage: bpmpkg,
}
}
}
}
// Determine words to be used for the following message
words := make([]string, 0)
if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool {
return action.GetActionType() == "install"
}) {
words = append(words, "Installing")
}
if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool {
return action.GetActionType() == "remove"
}) {
words = append(words, "Removing")
}
if len(words) == 0 {
return nil
}
fmt.Printf("%s packages...\n", strings.Join(words, "/"))
// Installing/Removing packages from system
for _, action := range operation.Actions {
if action.GetActionType() == "remove" {
pkgInfo := action.(*RemovePackageAction).BpmPackage.PkgInfo
err := removePackage(pkgInfo.Name, verbose, operation.RootDir)
if err != nil {
return errors.New(fmt.Sprintf("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
// Compile package if type is 'source'
if bpmpkg.PkgInfo.Type == "source" {
// Get path to compiled package directory
compiledDir := path.Join(operation.RootDir, "/var/cache/bpm/compiled/")
// Create compiled package directory if not exists
if _, err := os.Stat(compiledDir); err != nil {
err := os.MkdirAll(compiledDir, 0755)
if err != nil {
return err
}
}
// Get package name to install
pkgNameToInstall := bpmpkg.PkgInfo.Name
if bpmpkg.PkgInfo.IsSplitPackage() {
pkgNameToInstall = value.SplitPackageToInstall
}
// Compile source package if not compiled already
if _, ok := operation.compiledPackages[pkgNameToInstall]; !ok {
outputBpmPackages, err := CompileSourcePackage(value.File, compiledDir, false)
if err != nil {
return fmt.Errorf("could not compile source package (%s): %s\n", value.File, err)
}
// Add compiled packages to slice
for pkgName, pkgFile := range outputBpmPackages {
operation.compiledPackages[pkgName] = pkgFile
}
}
// Set values
fileToInstall = operation.compiledPackages[pkgNameToInstall]
bpmpkg, err = ReadPackage(fileToInstall)
if err != nil {
return fmt.Errorf("could not read package (%s): %s\n", fileToInstall, err)
}
}
if value.InstallationReason != InstallationReasonManual {
err = installPackage(fileToInstall, operation.RootDir, verbose, true)
} else {
err = installPackage(fileToInstall, operation.RootDir, verbose, force)
}
if err != nil {
return errors.New(fmt.Sprintf("could not install package (%s): %s\n", bpmpkg.PkgInfo.Name, err))
}
if !isReinstall {
err := SetInstallationReason(bpmpkg.PkgInfo.Name, value.InstallationReason, operation.RootDir)
if err != nil {
return errors.New(fmt.Sprintf("could not set installation reason for package (%s): %s\n", value.BpmPackage.PkgInfo.Name, err))
}
}
fmt.Printf("Package (%s) was successfully installed\n", bpmpkg.PkgInfo.Name)
}
}
fmt.Println("Operation complete!")
return nil
}
type OperationAction interface {
GetActionType() string
}
type InstallPackageAction struct {
File string
InstallationReason InstallationReason
SplitPackageToInstall string
BpmPackage *BPMPackage
}
func (action *InstallPackageAction) GetActionType() string {
return "install"
}
type FetchPackageAction struct {
InstallationReason InstallationReason
DatabaseEntry *BPMDatabaseEntry
}
func (action *FetchPackageAction) GetActionType() string {
return "fetch"
}
type RemovePackageAction struct {
BpmPackage *BPMPackage
}
func (action *RemovePackageAction) GetActionType() string {
return "remove"
}

1038
src/bpmlib/packages.go Normal file

File diff suppressed because it is too large Load Diff

209
src/bpmlib/tarball.go Normal file
View File

@ -0,0 +1,209 @@
package bpmlib
import (
"archive/tar"
"errors"
"io"
"os"
"path"
"strings"
)
type tarballFileReader struct {
tarReader *tar.Reader
file *os.File
}
func listTarballContent(tarballPath string) (content []string, err error) {
file, err := os.Open(tarballPath)
if err != nil {
return nil, err
}
defer file.Close()
tr := tar.NewReader(file)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
switch header.Typeflag {
case tar.TypeDir:
continue
default:
content = append(content, header.Name)
}
}
return content, nil
}
func readTarballFile(tarballPath, fileToExtract string) (*tarballFileReader, error) {
file, err := os.Open(tarballPath)
if err != nil {
return nil, err
}
tr := tar.NewReader(file)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if header.Name == fileToExtract {
if header.Typeflag != tar.TypeReg {
return nil, errors.New("file to extract must be a regular file")
}
return &tarballFileReader{
tarReader: tr,
file: file,
}, nil
}
}
return nil, errors.New("could not file in tarball")
}
func extractTarballFile(tarballPath, fileToExtract string, workingDirectory string, uid, gid int) (err error) {
file, err := os.Open(tarballPath)
if err != nil {
return err
}
defer file.Close()
tr := tar.NewReader(file)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
// Skip if filename does not match
if header.Name != fileToExtract {
continue
}
// Trim directory name from header name
header.Name = strings.Split(header.Name, "/")[len(strings.Split(header.Name, "/"))-1]
outputPath := path.Join(workingDirectory, header.Name)
switch header.Typeflag {
case tar.TypeReg:
// Create file and set permissions
file, err = os.Create(outputPath)
if err != nil {
return err
}
err := file.Chmod(header.FileInfo().Mode())
if err != nil {
return err
}
if uid >= 0 && gid >= 0 {
err = file.Chown(uid, gid)
if err != nil {
return err
}
}
// Copy data to file
_, err = io.Copy(file, tr)
if err != nil {
return err
}
// Close file
file.Close()
default:
continue
}
}
return nil
}
func extractTarballDirectory(tarballPath, directoryToExtract, workingDirectory string, uid, gid int) (err error) {
file, err := os.Open(tarballPath)
if err != nil {
return err
}
defer file.Close()
tr := tar.NewReader(file)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if strings.HasPrefix(header.Name, directoryToExtract+"/") {
// Skip directory to extract
if strings.TrimRight(header.Name, "/") == workingDirectory {
continue
}
// Trim directory name from header name
header.Name = strings.TrimPrefix(header.Name, directoryToExtract+"/")
outputPath := path.Join(workingDirectory, header.Name)
switch header.Typeflag {
case tar.TypeDir:
// Create directory
err := os.MkdirAll(outputPath, 0755)
if err != nil {
return err
}
// Set directory owner
if uid >= 0 && gid >= 0 {
err = os.Chown(outputPath, uid, gid)
if err != nil {
return err
}
}
case tar.TypeReg:
// Create file and set permissions
file, err = os.Create(outputPath)
if err != nil {
return err
}
err := file.Chmod(header.FileInfo().Mode())
if err != nil {
return err
}
if uid >= 0 && gid >= 0 {
err = file.Chown(uid, gid)
if err != nil {
return err
}
}
// Copy data to file
_, err = io.Copy(file, tr)
if err != nil {
return err
}
// Close file
file.Close()
default:
continue
}
}
}
return nil
}

View File

@ -1,10 +1,8 @@
package utils
package bpmlib
import (
"fmt"
"io"
"math"
"os"
"syscall"
)
@ -12,7 +10,7 @@ func GetArch() string {
uname := syscall.Utsname{}
err := syscall.Uname(&uname)
if err != nil {
return ""
return "unknown"
}
var byteString [65]byte
@ -23,29 +21,6 @@ func GetArch() string {
return string(byteString[:indexLength])
}
func copyFileContents(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
cerr := out.Close()
if err == nil {
err = cerr
}
}()
if _, err = io.Copy(out, in); err != nil {
return
}
err = out.Sync()
return
}
func stringSliceRemove(s []string, r string) []string {
for i, v := range s {
if v == r {
@ -55,7 +30,7 @@ func stringSliceRemove(s []string, r string) []string {
return s
}
func UnsignedBytesToHumanReadable(b uint64) string {
func unsignedBytesToHumanReadable(b uint64) string {
bf := float64(b)
for _, unit := range []string{"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"} {
if math.Abs(bf) < 1024.0 {
@ -66,7 +41,7 @@ func UnsignedBytesToHumanReadable(b uint64) string {
return fmt.Sprintf("%.1fYiB", bf)
}
func BytesToHumanReadable(b int64) string {
func bytesToHumanReadable(b int64) string {
bf := float64(b)
for _, unit := range []string{"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"} {
if math.Abs(bf) < 1024.0 {

View File

@ -1,49 +0,0 @@
package utils
import (
"gopkg.in/yaml.v3"
"log"
"os"
)
type BPMConfigStruct struct {
CompilationEnv []string `yaml:"compilation_env"`
SilentCompilation bool `yaml:"silent_compilation"`
BinaryOutputDir string `yaml:"binary_output_dir"`
CompilationDir string `yaml:"compilation_dir"`
Repositories []*Repository `yaml:"repositories"`
}
var BPMConfig BPMConfigStruct
func ReadConfig() {
if _, err := os.Stat("/etc/bpm.conf"); os.IsNotExist(err) {
log.Fatal(err)
}
bytes, err := os.ReadFile("/etc/bpm.conf")
if err != nil {
log.Fatal(err)
}
BPMConfig = BPMConfigStruct{
CompilationEnv: make([]string, 0),
SilentCompilation: false,
BinaryOutputDir: "/var/lib/bpm/compiled/",
CompilationDir: "/var/tmp/",
}
err = yaml.Unmarshal(bytes, &BPMConfig)
if err != nil {
log.Fatal(err)
}
for i := len(BPMConfig.Repositories) - 1; i >= 0; i-- {
if BPMConfig.Repositories[i].Disabled != nil && *BPMConfig.Repositories[i].Disabled {
BPMConfig.Repositories = append(BPMConfig.Repositories[:i], BPMConfig.Repositories[i+1:]...)
}
}
for _, repo := range BPMConfig.Repositories {
repo.Entries = make(map[string]*RepositoryEntry)
err := repo.ReadLocalDatabase()
if err != nil {
log.Fatal(err)
}
}
}

View File

@ -1,43 +0,0 @@
package utils
import (
"archive/tar"
"errors"
"io"
"os"
)
type TarballFileReader struct {
tarReader *tar.Reader
file *os.File
}
func ReadTarballContent(tarballPath, fileToExtract string) (*TarballFileReader, error) {
file, err := os.Open(tarballPath)
if err != nil {
return nil, err
}
tr := tar.NewReader(file)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if header.Name == fileToExtract {
if header.Typeflag != tar.TypeReg {
return nil, errors.New("file to extract must be a regular file")
}
return &TarballFileReader{
tarReader: tr,
file: file,
}, nil
}
}
return nil, errors.New("could not file in tarball")
}

View File

@ -1,289 +0,0 @@
package utils
import (
"errors"
"fmt"
"log"
"os"
"slices"
"strings"
)
type BPMOperation struct {
Actions []OperationAction
UnresolvedDepends []string
RootDir string
}
func (operation *BPMOperation) ActionsContainPackage(pkg string) bool {
for _, action := range operation.Actions {
if action.GetActionType() == "install" {
if action.(*InstallPackageAction).BpmPackage.PkgInfo.Name == pkg {
return true
}
} else if action.GetActionType() == "fetch" {
if action.(*FetchPackageAction).RepositoryEntry.Info.Name == pkg {
return true
}
} else if action.GetActionType() == "remove" {
if action.(*RemovePackageAction).BpmPackage.PkgInfo.Name == pkg {
return true
}
}
}
return false
}
func (operation *BPMOperation) InsertActionAt(index int, action OperationAction) {
if len(operation.Actions) == index { // nil or empty slice or after last element
operation.Actions = append(operation.Actions, action)
}
operation.Actions = append(operation.Actions[:index+1], operation.Actions[index:]...) // index < len(a)
operation.Actions[index] = action
}
func (operation *BPMOperation) GetTotalDownloadSize() uint64 {
var ret uint64 = 0
for _, action := range operation.Actions {
if action.GetActionType() == "fetch" {
ret += action.(*FetchPackageAction).RepositoryEntry.DownloadSize
}
}
return ret
}
func (operation *BPMOperation) GetTotalInstalledSize() uint64 {
var ret uint64 = 0
for _, action := range operation.Actions {
if action.GetActionType() == "install" {
ret += action.(*InstallPackageAction).BpmPackage.GetInstalledSize()
} else if action.GetActionType() == "fetch" {
ret += action.(*FetchPackageAction).RepositoryEntry.InstalledSize
}
}
return ret
}
func (operation *BPMOperation) GetFinalActionSize(rootDir string) int64 {
var ret int64 = 0
for _, action := range operation.Actions {
if action.GetActionType() == "install" {
ret += int64(action.(*InstallPackageAction).BpmPackage.GetInstalledSize())
if IsPackageInstalled(action.(*InstallPackageAction).BpmPackage.PkgInfo.Name, rootDir) {
ret -= int64(GetPackage(action.(*InstallPackageAction).BpmPackage.PkgInfo.Name, rootDir).GetInstalledSize())
}
} else if action.GetActionType() == "fetch" {
ret += int64(action.(*FetchPackageAction).RepositoryEntry.InstalledSize)
} else if action.GetActionType() == "remove" {
ret -= int64(action.(*RemovePackageAction).BpmPackage.GetInstalledSize())
}
}
return ret
}
func (operation *BPMOperation) ResolveDependencies(reinstallDependencies, installOptionalDependencies, verbose bool) error {
pos := 0
for _, value := range slices.Clone(operation.Actions) {
var pkgInfo *PackageInfo
if value.GetActionType() == "install" {
action := value.(*InstallPackageAction)
pkgInfo = action.BpmPackage.PkgInfo
} else if value.GetActionType() == "fetch" {
action := value.(*FetchPackageAction)
pkgInfo = action.RepositoryEntry.Info
} else {
pos++
continue
}
resolved, unresolved := pkgInfo.ResolveDependencies(&[]string{}, &[]string{}, pkgInfo.Type == "source", installOptionalDependencies, !reinstallDependencies, verbose, operation.RootDir)
operation.UnresolvedDepends = append(operation.UnresolvedDepends, unresolved...)
for _, depend := range resolved {
if !operation.ActionsContainPackage(depend) && depend != pkgInfo.Name {
if !reinstallDependencies && IsPackageInstalled(depend, operation.RootDir) {
continue
}
entry, _, err := GetRepositoryEntry(depend)
if err != nil {
return errors.New("could not get repository entry for package (" + depend + ")")
}
operation.InsertActionAt(pos, &FetchPackageAction{
IsDependency: true,
RepositoryEntry: entry,
})
pos++
}
}
pos++
}
return nil
}
func (operation *BPMOperation) ShowOperationSummary() {
if len(operation.Actions) == 0 {
fmt.Println("All packages are up to date!")
os.Exit(0)
}
for _, value := range operation.Actions {
var pkgInfo *PackageInfo
if value.GetActionType() == "install" {
pkgInfo = value.(*InstallPackageAction).BpmPackage.PkgInfo
} else if value.GetActionType() == "fetch" {
pkgInfo = value.(*FetchPackageAction).RepositoryEntry.Info
} else {
pkgInfo = value.(*RemovePackageAction).BpmPackage.PkgInfo
fmt.Printf("%s: %s (Remove)\n", pkgInfo.Name, pkgInfo.GetFullVersion())
continue
}
installedInfo := GetPackageInfo(pkgInfo.Name, operation.RootDir)
sourceInfo := ""
if pkgInfo.Type == "source" {
if operation.RootDir != "/" {
log.Fatalf("cannot compile and install source packages to a different root directory")
}
sourceInfo = "(From Source)"
}
if installedInfo == nil {
fmt.Printf("%s: %s (Install) %s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), sourceInfo)
} else {
comparison := ComparePackageVersions(*pkgInfo, *installedInfo)
if comparison < 0 {
fmt.Printf("%s: %s -> %s (Downgrade) %s\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), sourceInfo)
} else if comparison > 0 {
fmt.Printf("%s: %s -> %s (Upgrade) %s\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), sourceInfo)
} else {
fmt.Printf("%s: %s (Reinstall) %s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), sourceInfo)
}
}
}
if operation.RootDir != "/" {
fmt.Println("Warning: Operating in " + operation.RootDir)
}
if operation.GetTotalDownloadSize() > 0 {
fmt.Printf("%s will be downloaded to complete this operation\n", UnsignedBytesToHumanReadable(operation.GetTotalDownloadSize()))
}
if operation.GetFinalActionSize(operation.RootDir) > 0 {
fmt.Printf("A total of %s will be installed after the operation finishes\n", BytesToHumanReadable(operation.GetFinalActionSize(operation.RootDir)))
} else if operation.GetFinalActionSize(operation.RootDir) < 0 {
fmt.Printf("A total of %s will be freed after the operation finishes\n", strings.TrimPrefix(BytesToHumanReadable(operation.GetFinalActionSize(operation.RootDir)), "-"))
}
}
func (operation *BPMOperation) Execute(verbose, force bool) error {
// Fetch packages from repositories
if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool {
return action.GetActionType() == "fetch"
}) {
fmt.Println("Fetching packages from available repositories...")
for i, action := range operation.Actions {
if action.GetActionType() != "fetch" {
continue
}
entry := action.(*FetchPackageAction).RepositoryEntry
fetchedPackage, err := entry.Repository.FetchPackage(entry.Info.Name)
if err != nil {
return errors.New(fmt.Sprintf("could not fetch package (%s): %s\n", entry.Info.Name, err))
}
bpmpkg, err := ReadPackage(fetchedPackage)
if err != nil {
return errors.New(fmt.Sprintf("could not fetch package (%s): %s\n", entry.Info.Name, err))
}
fmt.Printf("Package (%s) was successfully fetched!\n", bpmpkg.PkgInfo.Name)
operation.Actions[i] = &InstallPackageAction{
File: fetchedPackage,
IsDependency: action.(*FetchPackageAction).IsDependency,
BpmPackage: bpmpkg,
}
}
}
// Determine words to be used for the following message
words := make([]string, 0)
if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool {
return action.GetActionType() == "install"
}) {
words = append(words, "Installing")
}
if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool {
return action.GetActionType() == "remove"
}) {
words = append(words, "Removing")
}
if len(words) == 0 {
return nil
}
fmt.Printf("%s packages...\n", strings.Join(words, "/"))
// Installing/Removing packages from system
for _, action := range operation.Actions {
if action.GetActionType() == "remove" {
pkgInfo := action.(*RemovePackageAction).BpmPackage.PkgInfo
err := RemovePackage(pkgInfo.Name, verbose, operation.RootDir)
if err != nil {
return errors.New(fmt.Sprintf("could not remove package (%s): %s\n", pkgInfo.Name, err))
}
} else if action.GetActionType() == "install" {
value := action.(*InstallPackageAction)
bpmpkg := value.BpmPackage
var err error
if value.IsDependency {
err = InstallPackage(value.File, operation.RootDir, verbose, true, false, false, false)
} else {
err = InstallPackage(value.File, operation.RootDir, verbose, force, false, false, false)
}
if err != nil {
return errors.New(fmt.Sprintf("could not install package (%s): %s\n", bpmpkg.PkgInfo.Name, err))
}
fmt.Printf("Package (%s) was successfully installed\n", bpmpkg.PkgInfo.Name)
if value.IsDependency {
err := SetInstallationReason(bpmpkg.PkgInfo.Name, Dependency, operation.RootDir)
if err != nil {
return errors.New(fmt.Sprintf("could not set installation reason for package (%s): %s\n", value.BpmPackage.PkgInfo.Name, err))
}
}
}
}
fmt.Println("Operation complete!")
return nil
}
type OperationAction interface {
GetActionType() string
}
type InstallPackageAction struct {
File string
IsDependency bool
BpmPackage *BPMPackage
}
func (action *InstallPackageAction) GetActionType() string {
return "install"
}
type FetchPackageAction struct {
IsDependency bool
RepositoryEntry *RepositoryEntry
}
func (action *FetchPackageAction) GetActionType() string {
return "fetch"
}
type RemovePackageAction struct {
BpmPackage *BPMPackage
}
func (action *RemovePackageAction) GetActionType() string {
return "remove"
}

File diff suppressed because it is too large Load Diff

View File

@ -1,197 +0,0 @@
package utils
import (
"errors"
"gopkg.in/yaml.v3"
"io"
"net/http"
"net/url"
"os"
"path"
"sort"
"strings"
)
type Repository struct {
Name string `yaml:"name"`
Source string `yaml:"source"`
Disabled *bool `yaml:"disabled"`
Entries map[string]*RepositoryEntry
}
type RepositoryEntry struct {
Info *PackageInfo `yaml:"info"`
Download string `yaml:"download"`
DownloadSize uint64 `yaml:"download_size"`
InstalledSize uint64 `yaml:"installed_size"`
IsVirtualPackage bool `yaml:"-"`
Repository *Repository
}
func (repo *Repository) ContainsPackage(pkg string) bool {
_, ok := repo.Entries[pkg]
return ok
}
func (repo *Repository) ReadLocalDatabase() error {
repoFile := "/var/lib/bpm/repositories/" + repo.Name + ".bpmdb"
if _, err := os.Stat(repoFile); err != nil {
return nil
}
bytes, err := os.ReadFile(repoFile)
if err != nil {
return err
}
virtualPackages := make(map[string][]string)
data := string(bytes)
for _, b := range strings.Split(data, "---") {
entry := RepositoryEntry{
Info: &PackageInfo{
Name: "",
Description: "",
Version: "",
Revision: 1,
Url: "",
License: "",
Arch: "",
Type: "",
Keep: make([]string, 0),
Depends: make([]string, 0),
MakeDepends: make([]string, 0),
OptionalDepends: make([]string, 0),
Conflicts: make([]string, 0),
Provides: make([]string, 0),
},
Download: "",
DownloadSize: 0,
InstalledSize: 0,
IsVirtualPackage: false,
Repository: repo,
}
err := yaml.Unmarshal([]byte(b), &entry)
if err != nil {
return err
}
for _, p := range entry.Info.Provides {
virtualPackages[p] = append(virtualPackages[p], entry.Info.Name)
}
repo.Entries[entry.Info.Name] = &entry
}
for key, value := range virtualPackages {
if _, ok := repo.Entries[key]; ok {
continue
}
sort.Strings(value)
entry := RepositoryEntry{
Info: repo.Entries[value[0]].Info,
Download: repo.Entries[value[0]].Download,
DownloadSize: repo.Entries[value[0]].DownloadSize,
InstalledSize: repo.Entries[value[0]].InstalledSize,
IsVirtualPackage: true,
Repository: repo,
}
repo.Entries[key] = &entry
}
return nil
}
func (repo *Repository) SyncLocalDatabase() error {
repoFile := "/var/lib/bpm/repositories/" + repo.Name + ".bpmdb"
err := os.MkdirAll(path.Dir(repoFile), 0755)
if err != nil {
return err
}
u, err := url.JoinPath(repo.Source, "database.bpmdb")
if err != nil {
return err
}
resp, err := http.Get(u)
if err != nil {
return err
}
defer resp.Body.Close()
out, err := os.Create(repoFile)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return nil
}
func GetRepository(name string) *Repository {
for _, repo := range BPMConfig.Repositories {
if repo.Name == name {
return repo
}
}
return nil
}
func GetRepositoryEntry(str string) (*RepositoryEntry, *Repository, error) {
split := strings.Split(str, "/")
if len(split) == 1 {
pkgName := strings.TrimSpace(split[0])
if pkgName == "" {
return nil, nil, errors.New("could not find repository entry for this package")
}
for _, repo := range BPMConfig.Repositories {
if repo.ContainsPackage(pkgName) {
return repo.Entries[pkgName], repo, nil
}
}
return nil, nil, errors.New("could not find repository entry for this package")
} else if len(split) == 2 {
repoName := strings.TrimSpace(split[0])
pkgName := strings.TrimSpace(split[1])
if repoName == "" || pkgName == "" {
return nil, nil, errors.New("could not find repository entry for this package")
}
repo := GetRepository(repoName)
if repo == nil || !repo.ContainsPackage(pkgName) {
return nil, nil, errors.New("could not find repository entry for this package")
}
return repo.Entries[pkgName], repo, nil
} else {
return nil, nil, errors.New("could not find repository entry for this package")
}
}
func (repo *Repository) FetchPackage(pkg string) (string, error) {
if !repo.ContainsPackage(pkg) {
return "", errors.New("could not fetch package '" + pkg + "'")
}
entry := repo.Entries[pkg]
URL, err := url.JoinPath(repo.Source, entry.Download)
if err != nil {
return "", err
}
resp, err := http.Get(URL)
if err != nil {
return "", err
}
defer resp.Body.Close()
err = os.MkdirAll("/var/cache/bpm/packages/", 0755)
if err != nil {
return "", err
}
out, err := os.Create("/var/cache/bpm/packages/" + path.Base(entry.Download))
if err != nil {
return "", err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return "/var/cache/bpm/packages/" + path.Base(entry.Download), nil
}