- Added source package support

- Added 2 new flags for 'bpm list'
- Added a new Package Creation section in README.md
This commit is contained in:
CapCreeperGR 2024-03-28 22:15:42 +02:00
parent b6d217819c
commit 036578e652
10 changed files with 354 additions and 63 deletions

View File

@ -5,7 +5,7 @@ BPM is a simple package manager for Linux systems
## Features ## Features
- Simple to use subcommands - Simple to use subcommands
- Can install binary and source packages (source packages are still under development) - Can install binary and source packages
- Can be easily installed on practically any system - Can be easily installed on practically any system
- No bloat - No bloat
@ -47,3 +47,48 @@ For information on the rest of the commands simply use the help command or pass
``` ```
bpm help bpm help
``` ```
## Package Creation
Creating a package for BPM is simple
To create a package you need to
1) Create a working directory
```
mkdir my_bpm_package
```
2) Create a pkg.info file following this format (You can find examples in the test_packages directory)
```
name: my_package
description: My package's description
version: 1.0
architecture: x86_64
type: <binary/source>
depends: dependency1,dependency2
make_depends: make_depend1,make_depend2
```
depends and make depends are optional fields, you may skip them if you'd like
### Binary Packages
3) If you are making a binary package, simply create a 'files' directory
```
mkdir files
```
4) Copy all your binaries along with the directories they reside in (i.e files/usr/bin/my_binary)
5) Either copy the bpm-create script from the bpm-utils test package into your /usr/local/bin directory or install the bpm-utils.bpm package
6) Run the following
```
bpm-create <filename_without_extension>
```
7) It's done! You now hopefully have a working BPM package!
### Source Packages
3) If you are making a source package, you need to create a 'source.sh' file
```
touch source.sh
```
4) You are able to run bash code in this file. BPM will extract this file in a directory under /tmp and it will be ran there
5) Your goal is to download your program's source code with either git, wget, curl, etc. and put the binaries under a folder called 'output' in the root of the temp directory. There is a simple example script with helpful comments in the htop-src test package
6) As of this moment there is no script to automate package compression like for binary packages. You will need to create the archive manually
```
tar -czvf my_package-src.bpm pkg.info source.sh
```
7) That's it! Your source package should now be compiling correctly!

View File

@ -10,7 +10,7 @@ func GetArch() string {
if err != nil { if err != nil {
return "" return ""
} }
return strings.TrimSpace(byteArrayToString(output)) return strings.TrimSpace(string(output))
} }
func stringSliceRemove(s []string, r string) []string { func stringSliceRemove(s []string, r string) []string {

View File

@ -7,9 +7,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"log" "log"
"os" "os"
"os/exec"
"path" "path"
"path/filepath"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@ -22,6 +25,7 @@ type PackageInfo struct {
Arch string Arch string
Type string Type string
Depends []string Depends []string
MakeDepends []string
Provides []string Provides []string
} }
@ -70,6 +74,7 @@ func ReadPackageInfo(contents string, defaultValues bool) (*PackageInfo, error)
Arch: "", Arch: "",
Type: "", Type: "",
Depends: nil, Depends: nil,
MakeDepends: nil,
Provides: nil, Provides: nil,
} }
lines := strings.Split(contents, "\n") lines := strings.Split(contents, "\n")
@ -97,9 +102,12 @@ func ReadPackageInfo(contents string, defaultValues bool) (*PackageInfo, error)
case "depends": case "depends":
pkgInfo.Depends = strings.Split(strings.Replace(split[1], " ", "", -1), ",") pkgInfo.Depends = strings.Split(strings.Replace(split[1], " ", "", -1), ",")
pkgInfo.Depends = stringSliceRemoveEmpty(pkgInfo.Depends) pkgInfo.Depends = stringSliceRemoveEmpty(pkgInfo.Depends)
case "make_depends":
pkgInfo.MakeDepends = strings.Split(strings.Replace(split[1], " ", "", -1), ",")
pkgInfo.MakeDepends = stringSliceRemoveEmpty(pkgInfo.MakeDepends)
case "provides": case "provides":
pkgInfo.Provides = strings.Split(strings.Replace(split[1], " ", "", -1), ",") pkgInfo.Provides = strings.Split(strings.Replace(split[1], " ", "", -1), ",")
pkgInfo.Provides = stringSliceRemoveEmpty(pkgInfo.Depends) pkgInfo.Provides = stringSliceRemoveEmpty(pkgInfo.Provides)
} }
} }
if !defaultValues { if !defaultValues {
@ -160,6 +168,7 @@ func InstallPackage(filename, installDir string, force bool) error {
return errors.New("Could not resolve all dependencies. Missing " + strings.Join(unresolved, ", ")) return errors.New("Could not resolve all dependencies. Missing " + strings.Join(unresolved, ", "))
} }
} }
if pkgInfo.Type == "binary" {
for { for {
header, err := tr.Next() header, err := tr.Next()
if err == io.EOF { if err == io.EOF {
@ -173,11 +182,16 @@ func InstallPackage(filename, installDir string, force bool) error {
switch header.Typeflag { switch header.Typeflag {
case tar.TypeDir: case tar.TypeDir:
files = append(files, strings.TrimPrefix(header.Name, "files/")) files = append(files, strings.TrimPrefix(header.Name, "files/"))
if err := os.Mkdir(extractFilename, 0755); err != nil && !os.IsExist(err) { if err := os.Mkdir(extractFilename, 0755); err != nil {
if !os.IsExist(err) {
return err return err
} }
} else {
fmt.Println("Creating Directory: " + extractFilename)
}
case tar.TypeReg: case tar.TypeReg:
outFile, err := os.Create(extractFilename) outFile, err := os.Create(extractFilename)
fmt.Println("Creating File: " + extractFilename)
files = append(files, strings.TrimPrefix(header.Name, "files/")) files = append(files, strings.TrimPrefix(header.Name, "files/"))
if err != nil { if err != nil {
return err return err
@ -193,19 +207,125 @@ func InstallPackage(filename, installDir string, force bool) error {
return err return err
} }
case tar.TypeSymlink: case tar.TypeSymlink:
fmt.Println("old name: " + header.Linkname) fmt.Println("Creating Symlink: "+extractFilename, " -> "+header.Linkname)
fmt.Println("new name: " + header.Name) files = append(files, strings.TrimPrefix(header.Name, "files/"))
err := os.Symlink(header.Linkname, extractFilename) err := os.Symlink(header.Linkname, extractFilename)
if err != nil { if err != nil {
return err return err
} }
default: default:
return errors.New("ExtractTarGz: uknown type: " + strconv.Itoa(int(header.Typeflag)) + " in " + extractFilename) return errors.New("ExtractTarGz: unknown type: " + strconv.Itoa(int(header.Typeflag)) + " in " + extractFilename)
} }
} }
} }
if pkgInfo == nil { } else if pkgInfo.Type == "source" {
return errors.New("pkg.info not found in archive") for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if header.Name == "source.sh" {
bs, err := io.ReadAll(tr)
if err != nil {
return err
}
temp, err := os.MkdirTemp("/tmp/", "bpm_source-")
fmt.Println("Creating temp directory at: " + temp)
if err != nil {
return err
}
err = os.WriteFile(path.Join(temp, "source.sh"), bs, 0644)
if err != nil {
return err
}
fmt.Println("Running source.sh file...")
cmd := exec.Command("/usr/bin/sh", "source.sh")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = temp
err = cmd.Run()
if err != nil {
return err
}
if _, err := os.Stat(path.Join(temp, "/output/")); err != nil {
if os.IsNotExist(err) {
return errors.New("Output directory not be found at " + path.Join(temp, "/output/"))
}
return err
}
fmt.Println("Copying all files...")
err = filepath.WalkDir(path.Join(temp, "/output/"), func(fullpath string, d fs.DirEntry, err error) error {
relFilename, err := filepath.Rel(path.Join(temp, "/output/"), fullpath)
if relFilename == "." {
return nil
}
extractFilename := path.Join(installDir, relFilename)
if err != nil {
return err
}
if d.Type() == os.ModeDir {
files = append(files, relFilename+"/")
if err := os.Mkdir(extractFilename, 0755); err != nil {
if !os.IsExist(err) {
return err
}
} else {
fmt.Println("Creating Directory: " + extractFilename)
}
} else if d.Type().IsRegular() {
outFile, err := os.Create(extractFilename)
fmt.Println("Creating File: " + extractFilename)
files = append(files, relFilename)
if err != nil {
return err
}
f, err := os.Open(fullpath)
if err != nil {
return err
}
if _, err := io.Copy(outFile, f); err != nil {
return err
}
info, err := os.Stat(fullpath)
if err != nil {
return err
}
if err := os.Chmod(extractFilename, info.Mode()); err != nil {
return err
}
err = outFile.Close()
if err != nil {
return err
}
err = f.Close()
if err != nil {
return err
}
} else if d.Type() == os.ModeSymlink {
link, err := os.Readlink(fullpath)
if err != nil {
return err
}
fmt.Println("Creating Symlink: "+extractFilename, " -> "+link)
files = append(files, relFilename)
err = os.Symlink(link, extractFilename)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
}
}
} else {
return errors.New("Unknown package type: " + pkgInfo.Type)
} }
slices.Sort(files) slices.Sort(files)
slices.Reverse(files) slices.Reverse(files)
@ -264,6 +384,47 @@ func InstallPackage(filename, installDir string, force bool) error {
return nil return nil
} }
func GetSourceScript(filename string) (string, error) {
pkgInfo, err := ReadPackage(filename)
if err != nil {
return "", err
}
if pkgInfo.Type != "source" {
return "", errors.New("package not of source type")
}
file, err := os.Open(filename)
if err != nil {
return "", err
}
archive, err := gzip.NewReader(file)
if err != nil {
return "", err
}
tr := tar.NewReader(archive)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if header.Name == "source.sh" {
err := archive.Close()
if err != nil {
return "", err
}
err = file.Close()
if err != nil {
return "", err
}
bs, err := io.ReadAll(tr)
if err != nil {
return "", err
}
return string(bs), nil
}
}
return "", errors.New("package does not contain a source.sh file")
}
func CheckDependencies(pkgInfo *PackageInfo, rootDir string) []string { func CheckDependencies(pkgInfo *PackageInfo, rootDir string) []string {
unresolved := make([]string, len(pkgInfo.Depends)) unresolved := make([]string, len(pkgInfo.Depends))
copy(unresolved, pkgInfo.Depends) copy(unresolved, pkgInfo.Depends)
@ -287,6 +448,29 @@ func CheckDependencies(pkgInfo *PackageInfo, rootDir string) []string {
return unresolved return unresolved
} }
func CheckMakeDependencies(pkgInfo *PackageInfo, rootDir string) []string {
unresolved := make([]string, len(pkgInfo.MakeDepends))
copy(unresolved, pkgInfo.MakeDepends)
installedDir := path.Join(rootDir, "var/lib/bpm/installed/")
if _, err := os.Stat(installedDir); err != nil {
return nil
}
items, err := os.ReadDir(installedDir)
if err != nil {
return nil
}
for _, item := range items {
if !item.IsDir() {
continue
}
if slices.Contains(unresolved, item.Name()) {
unresolved = stringSliceRemove(unresolved, item.Name())
}
}
return unresolved
}
func IsPackageInstalled(pkg, rootDir string) bool { func IsPackageInstalled(pkg, rootDir string) bool {
installedDir := path.Join(rootDir, "var/lib/bpm/installed/") installedDir := path.Join(rootDir, "var/lib/bpm/installed/")
pkgDir := path.Join(installedDir, pkg) pkgDir := path.Join(installedDir, pkg)

51
main.go
View File

@ -17,7 +17,7 @@ import (
/* A simple-to-use package manager */ /* A simple-to-use package manager */
/* ---------------------------------- */ /* ---------------------------------- */
var bpmVer = "0.0.7" var bpmVer = "0.0.8"
var rootDir = "/" var rootDir = "/"
func main() { func main() {
@ -105,7 +105,7 @@ func resolveCommand() {
} }
} }
case list: case list:
resolveFlags() flags, _ := resolveFlags()
packages, err := bpm_utils.GetInstalledPackages(rootDir) packages, err := bpm_utils.GetInstalledPackages(rootDir)
if err != nil { if err != nil {
log.Fatalf("Could not get installed packages\nError: %s", err.Error()) log.Fatalf("Could not get installed packages\nError: %s", err.Error())
@ -115,6 +115,13 @@ func resolveCommand() {
fmt.Println("No packages have been installed") fmt.Println("No packages have been installed")
return return
} }
if slices.Contains(flags, "n") {
fmt.Println(len(packages))
} else if slices.Contains(flags, "l") {
for _, pkg := range packages {
fmt.Println(pkg)
}
} else {
for n, pkg := range packages { for n, pkg := range packages {
info := bpm_utils.GetPackageInfo(pkg, rootDir, false) info := bpm_utils.GetPackageInfo(pkg, rootDir, false)
if info == nil { if info == nil {
@ -126,6 +133,7 @@ func resolveCommand() {
fmt.Println("----------------") fmt.Println("----------------")
} }
} }
}
case install: case install:
flags, i := resolveFlags() flags, i := resolveFlags()
files := getArgs()[1+i:] files := getArgs()[1+i:]
@ -140,15 +148,25 @@ func resolveCommand() {
} }
fmt.Print("----------------\n" + bpm_utils.CreateInfoFile(*pkgInfo)) fmt.Print("----------------\n" + bpm_utils.CreateInfoFile(*pkgInfo))
fmt.Println("----------------") fmt.Println("----------------")
verb := "install"
if pkgInfo.Type == "source" {
verb = "build"
}
if !slices.Contains(flags, "f") { if !slices.Contains(flags, "f") {
if pkgInfo.Arch != bpm_utils.GetArch() { if pkgInfo.Arch != bpm_utils.GetArch() {
fmt.Println("skipping... cannot install a package with a different architecture") fmt.Printf("skipping... cannot %s a package with a different architecture\n", verb)
continue continue
} }
if unresolved := bpm_utils.CheckDependencies(pkgInfo, rootDir); len(unresolved) != 0 { if unresolved := bpm_utils.CheckDependencies(pkgInfo, rootDir); len(unresolved) != 0 {
fmt.Printf("skipping... cannot install package (%s) due to missing dependencies: %s\n", pkgInfo.Name, strings.Join(unresolved, ", ")) fmt.Printf("skipping... cannot %s package (%s) due to missing dependencies: %s\n", verb, pkgInfo.Name, strings.Join(unresolved, ", "))
continue continue
} }
if pkgInfo.Type == "source" {
if unresolved := bpm_utils.CheckMakeDependencies(pkgInfo, rootDir); len(unresolved) != 0 {
fmt.Printf("skipping... cannot %s package (%s) due to missing make dependencies: %s\n", verb, pkgInfo.Name, strings.Join(unresolved, ", "))
continue
}
}
} }
if bpm_utils.IsPackageInstalled(pkgInfo.Name, rootDir) { if bpm_utils.IsPackageInstalled(pkgInfo.Name, rootDir) {
if !slices.Contains(flags, "y") { if !slices.Contains(flags, "y") {
@ -161,7 +179,7 @@ func resolveCommand() {
fmt.Print("Do you wish to downgrade this package? (Not recommended) [y\\N] ") fmt.Print("Do you wish to downgrade this package? (Not recommended) [y\\N] ")
} else if strings.Compare(pkgInfo.Version, installedInfo.Version) == 0 { } else if strings.Compare(pkgInfo.Version, installedInfo.Version) == 0 {
fmt.Println("This package is already installed on the system and is up to date") fmt.Println("This package is already installed on the system and is up to date")
fmt.Print("Do you wish to reinstall this package? [y\\N] ") fmt.Printf("Do you wish to re%s this package? [y\\N] ", verb)
} }
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n') text, _ := reader.ReadString('\n')
@ -175,8 +193,21 @@ func resolveCommand() {
log.Fatalf("Could not remove current version of the package\nError: %s\n", err) log.Fatalf("Could not remove current version of the package\nError: %s\n", err)
} }
} else if !slices.Contains(flags, "y") { } else if !slices.Contains(flags, "y") {
fmt.Print("Do you wish to install this package? [y\\N] ")
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
if pkgInfo.Type == "source" {
fmt.Print("Would you like to view the source.sh file of this package? [Y\\n]")
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "n" && strings.TrimSpace(strings.ToLower(text)) != "no" {
script, err := bpm_utils.GetSourceScript(file)
if err != nil {
log.Fatalf("Could not read source script\nError: %s\n", err)
}
fmt.Println(script)
fmt.Println("-------EOF-------")
}
}
fmt.Printf("Do you wish to %s this package? [y\\N] ", verb)
text, _ := reader.ReadString('\n') text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" { if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Printf("Skipping package (%s)...\n", pkgInfo.Name) fmt.Printf("Skipping package (%s)...\n", pkgInfo.Name)
@ -229,7 +260,7 @@ func resolveCommand() {
fmt.Println("\033[1m\\ Command List /\033[0m") fmt.Println("\033[1m\\ Command List /\033[0m")
fmt.Println("-> bpm version | shows information on the installed version of bpm") fmt.Println("-> bpm version | shows information on the installed version of bpm")
fmt.Println("-> bpm info | shows information on an installed package") fmt.Println("-> bpm info | shows information on an installed package")
fmt.Println("-> bpm list | lists all installed packages") fmt.Println("-> bpm list [-n, -l] | lists all installed packages. -n shows the number of packages. -l lists package names only")
fmt.Println("-> bpm install [-y, -f] <files...> | installs the following files. -y skips the confirmation prompt. -f skips dependency and architecture checking") fmt.Println("-> bpm install [-y, -f] <files...> | installs the following files. -y skips the confirmation prompt. -f skips dependency and architecture checking")
fmt.Println("-> bpm remove [-y] <packages...> | removes the following packages. -y skips the confirmation prompt") fmt.Println("-> bpm remove [-y] <packages...> | removes the following packages. -y skips the confirmation prompt")
fmt.Println("-> bpm cleanup | removes all unneeded dependencies") fmt.Println("-> bpm cleanup | removes all unneeded dependencies")
@ -246,6 +277,12 @@ func resolveFlags() ([]string, int) {
switch getCommandType() { switch getCommandType() {
default: default:
log.Fatalf("Invalid flag " + flag) log.Fatalf("Invalid flag " + flag)
case list:
v := [...]string{"l", "n"}
if !slices.Contains(v[:], f) {
log.Fatalf("Invalid flag " + flag)
}
ret = append(ret, f)
case install: case install:
v := [...]string{"y", "f"} v := [...]string{"y", "f"}
if !slices.Contains(v[:], f) { if !slices.Contains(v[:], f) {

Binary file not shown.

View File

@ -1,5 +1,5 @@
name: bpm name: bpm
description: The Bubble Package Manager description: The Bubble Package Manager
version: 0.0.7 version: 0.0.8
architecture: x86_64 architecture: x86_64
type: binary type: binary

Binary file not shown.

View File

@ -0,0 +1,5 @@
name: htop
description: An interactive process viewer
version: 3.3.0
architecture: x86_64
type: source

View File

@ -0,0 +1,20 @@
# This file is read and executed by BPM to compile htop. It will run inside a temporary folder in /tmp during execution
echo "Building htop..."
# Creating 'source' directory
mkdir source
# Cloning the git repository into the 'source' directory
git clone https://github.com/htop-dev/htop.git source
# Changing directory into 'source'
cd source
# Configuring and making htop according to the installation instructions in the repository
./autogen.sh
./configure --prefix=/usr
make
# Creating an 'output' directory in the root of the temporary directory created by BPM
mkdir ./../output/
# Setting $dir to the 'output' directory
dir=$(pwd)/../output/
# Installing htop to $dir
make DESTDIR="$dir" install
# The compilation is done. BPM will now copy the files from the 'output' directory into the root of your system
echo "htop compilation complete!"