diff --git a/README.md b/README.md index 6af3b05..68cf6f3 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ BPM is a simple package manager for Linux systems ## Features - Simple to use subcommands -- Can install binary 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 -``` \ No newline at end of file +``` + +## 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: +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 +``` +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! \ No newline at end of file diff --git a/bpm_utils/general_utils.go b/bpm_utils/general_utils.go index 2b74ece..23eb6e6 100644 --- a/bpm_utils/general_utils.go +++ b/bpm_utils/general_utils.go @@ -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 { diff --git a/bpm_utils/package_utils.go b/bpm_utils/package_utils.go index ea18f1f..a203ff9 100644 --- a/bpm_utils/package_utils.go +++ b/bpm_utils/package_utils.go @@ -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) diff --git a/main.go b/main.go index b6f3528..19df223 100644 --- a/main.go +++ b/main.go @@ -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] | installs the following files. -y skips the confirmation prompt. -f skips dependency and architecture checking") fmt.Println("-> bpm remove [-y] | 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) { diff --git a/test_packages/x86_64/bpm/bpm.bpm b/test_packages/x86_64/bpm/bpm.bpm index fe44639..98f1107 100644 Binary files a/test_packages/x86_64/bpm/bpm.bpm and b/test_packages/x86_64/bpm/bpm.bpm differ diff --git a/test_packages/x86_64/bpm/files/usr/bin/bpm b/test_packages/x86_64/bpm/files/usr/bin/bpm index 9757cc2..2c08237 100755 Binary files a/test_packages/x86_64/bpm/files/usr/bin/bpm and b/test_packages/x86_64/bpm/files/usr/bin/bpm differ diff --git a/test_packages/x86_64/bpm/pkg.info b/test_packages/x86_64/bpm/pkg.info index 3a73b8c..95068da 100644 --- a/test_packages/x86_64/bpm/pkg.info +++ b/test_packages/x86_64/bpm/pkg.info @@ -1,5 +1,5 @@ name: bpm description: The Bubble Package Manager -version: 0.0.7 +version: 0.0.8 architecture: x86_64 type: binary diff --git a/test_packages/x86_64/htop-src/htop-src.bpm b/test_packages/x86_64/htop-src/htop-src.bpm new file mode 100644 index 0000000..3bc1a92 Binary files /dev/null and b/test_packages/x86_64/htop-src/htop-src.bpm differ diff --git a/test_packages/x86_64/htop-src/pkg.info b/test_packages/x86_64/htop-src/pkg.info new file mode 100644 index 0000000..0709896 --- /dev/null +++ b/test_packages/x86_64/htop-src/pkg.info @@ -0,0 +1,5 @@ +name: htop +description: An interactive process viewer +version: 3.3.0 +architecture: x86_64 +type: source diff --git a/test_packages/x86_64/htop-src/source.sh b/test_packages/x86_64/htop-src/source.sh new file mode 100644 index 0000000..2dda643 --- /dev/null +++ b/test_packages/x86_64/htop-src/source.sh @@ -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!"