- 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
- 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
- No bloat
@ -46,4 +46,49 @@ The -y flag applies here as well if you wish to skip the removal confirmation pr
For information on the rest of the commands simply use the help command or pass in no arguments at all
```
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 {
return ""
}
return strings.TrimSpace(byteArrayToString(output))
return strings.TrimSpace(string(output))
}
func stringSliceRemove(s []string, r string) []string {

View File

@ -7,9 +7,12 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"log"
"os"
"os/exec"
"path"
"path/filepath"
"slices"
"strconv"
"strings"
@ -22,6 +25,7 @@ type PackageInfo struct {
Arch string
Type string
Depends []string
MakeDepends []string
Provides []string
}
@ -70,6 +74,7 @@ func ReadPackageInfo(contents string, defaultValues bool) (*PackageInfo, error)
Arch: "",
Type: "",
Depends: nil,
MakeDepends: nil,
Provides: nil,
}
lines := strings.Split(contents, "\n")
@ -97,9 +102,12 @@ func ReadPackageInfo(contents string, defaultValues bool) (*PackageInfo, error)
case "depends":
pkgInfo.Depends = strings.Split(strings.Replace(split[1], " ", "", -1), ",")
pkgInfo.Depends = stringSliceRemoveEmpty(pkgInfo.Depends)
case "make_depends":
pkgInfo.MakeDepends = strings.Split(strings.Replace(split[1], " ", "", -1), ",")
pkgInfo.MakeDepends = stringSliceRemoveEmpty(pkgInfo.MakeDepends)
case "provides":
pkgInfo.Provides = strings.Split(strings.Replace(split[1], " ", "", -1), ",")
pkgInfo.Provides = stringSliceRemoveEmpty(pkgInfo.Depends)
pkgInfo.Provides = stringSliceRemoveEmpty(pkgInfo.Provides)
}
}
if !defaultValues {
@ -160,52 +168,164 @@ func InstallPackage(filename, installDir string, force bool) error {
return errors.New("Could not resolve all dependencies. Missing " + strings.Join(unresolved, ", "))
}
}
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if strings.HasPrefix(header.Name, "files/") && header.Name != "files/" {
extractFilename := path.Join(installDir, strings.TrimPrefix(header.Name, "files/"))
switch header.Typeflag {
case tar.TypeDir:
files = append(files, strings.TrimPrefix(header.Name, "files/"))
if err := os.Mkdir(extractFilename, 0755); err != nil && !os.IsExist(err) {
return err
if pkgInfo.Type == "binary" {
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if strings.HasPrefix(header.Name, "files/") && header.Name != "files/" {
extractFilename := path.Join(installDir, strings.TrimPrefix(header.Name, "files/"))
switch header.Typeflag {
case tar.TypeDir:
files = append(files, strings.TrimPrefix(header.Name, "files/"))
if err := os.Mkdir(extractFilename, 0755); err != nil {
if !os.IsExist(err) {
return err
}
} else {
fmt.Println("Creating Directory: " + extractFilename)
}
case tar.TypeReg:
outFile, err := os.Create(extractFilename)
fmt.Println("Creating File: " + extractFilename)
files = append(files, strings.TrimPrefix(header.Name, "files/"))
if err != nil {
return err
}
if _, err := io.Copy(outFile, tr); err != nil {
return err
}
if err := os.Chmod(extractFilename, header.FileInfo().Mode()); err != nil {
return err
}
err = outFile.Close()
if err != nil {
return err
}
case tar.TypeSymlink:
fmt.Println("Creating Symlink: "+extractFilename, " -> "+header.Linkname)
files = append(files, strings.TrimPrefix(header.Name, "files/"))
err := os.Symlink(header.Linkname, extractFilename)
if err != nil {
return err
}
default:
return errors.New("ExtractTarGz: unknown type: " + strconv.Itoa(int(header.Typeflag)) + " in " + extractFilename)
}
case tar.TypeReg:
outFile, err := os.Create(extractFilename)
files = append(files, strings.TrimPrefix(header.Name, "files/"))
if err != nil {
return err
}
if _, err := io.Copy(outFile, tr); err != nil {
return err
}
if err := os.Chmod(extractFilename, header.FileInfo().Mode()); err != nil {
return err
}
err = outFile.Close()
if err != nil {
return err
}
case tar.TypeSymlink:
fmt.Println("old name: " + header.Linkname)
fmt.Println("new name: " + header.Name)
err := os.Symlink(header.Linkname, extractFilename)
if err != nil {
return err
}
default:
return errors.New("ExtractTarGz: uknown type: " + strconv.Itoa(int(header.Typeflag)) + " in " + extractFilename)
}
}
}
if pkgInfo == nil {
return errors.New("pkg.info not found in archive")
} else if pkgInfo.Type == "source" {
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.Reverse(files)
@ -264,6 +384,47 @@ func InstallPackage(filename, installDir string, force bool) error {
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 {
unresolved := make([]string, len(pkgInfo.Depends))
copy(unresolved, pkgInfo.Depends)
@ -287,6 +448,29 @@ func CheckDependencies(pkgInfo *PackageInfo, rootDir string) []string {
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 {
installedDir := path.Join(rootDir, "var/lib/bpm/installed/")
pkgDir := path.Join(installedDir, pkg)

67
main.go
View File

@ -17,7 +17,7 @@ import (
/* A simple-to-use package manager */
/* ---------------------------------- */
var bpmVer = "0.0.7"
var bpmVer = "0.0.8"
var rootDir = "/"
func main() {
@ -105,7 +105,7 @@ func resolveCommand() {
}
}
case list:
resolveFlags()
flags, _ := resolveFlags()
packages, err := bpm_utils.GetInstalledPackages(rootDir)
if err != nil {
log.Fatalf("Could not get installed packages\nError: %s", err.Error())
@ -115,15 +115,23 @@ func resolveCommand() {
fmt.Println("No packages have been installed")
return
}
for n, pkg := range packages {
info := bpm_utils.GetPackageInfo(pkg, rootDir, false)
if info == nil {
fmt.Printf("Package (%s) could not be found\n", pkg)
continue
if slices.Contains(flags, "n") {
fmt.Println(len(packages))
} else if slices.Contains(flags, "l") {
for _, pkg := range packages {
fmt.Println(pkg)
}
fmt.Print("----------------\n" + bpm_utils.CreateInfoFile(*info))
if n == len(packages)-1 {
fmt.Println("----------------")
} else {
for n, pkg := range packages {
info := bpm_utils.GetPackageInfo(pkg, rootDir, false)
if info == nil {
fmt.Printf("Package (%s) could not be found\n", pkg)
continue
}
fmt.Print("----------------\n" + bpm_utils.CreateInfoFile(*info))
if n == len(packages)-1 {
fmt.Println("----------------")
}
}
}
case install:
@ -140,15 +148,25 @@ func resolveCommand() {
}
fmt.Print("----------------\n" + bpm_utils.CreateInfoFile(*pkgInfo))
fmt.Println("----------------")
verb := "install"
if pkgInfo.Type == "source" {
verb = "build"
}
if !slices.Contains(flags, "f") {
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
}
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
}
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 !slices.Contains(flags, "y") {
@ -161,7 +179,7 @@ func resolveCommand() {
fmt.Print("Do you wish to downgrade this package? (Not recommended) [y\\N] ")
} 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.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)
text, _ := reader.ReadString('\n')
@ -175,8 +193,21 @@ func resolveCommand() {
log.Fatalf("Could not remove current version of the package\nError: %s\n", err)
}
} else if !slices.Contains(flags, "y") {
fmt.Print("Do you wish to install this package? [y\\N] ")
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')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Printf("Skipping package (%s)...\n", pkgInfo.Name)
@ -229,7 +260,7 @@ func resolveCommand() {
fmt.Println("\033[1m\\ Command List /\033[0m")
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 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 remove [-y] <packages...> | removes the following packages. -y skips the confirmation prompt")
fmt.Println("-> bpm cleanup | removes all unneeded dependencies")
@ -246,6 +277,12 @@ func resolveFlags() ([]string, int) {
switch getCommandType() {
default:
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:
v := [...]string{"y", "f"}
if !slices.Contains(v[:], f) {

Binary file not shown.

View File

@ -1,5 +1,5 @@
name: bpm
description: The Bubble Package Manager
version: 0.0.7
version: 0.0.8
architecture: x86_64
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!"