diff --git a/src/bpm/main.go b/src/bpm/main.go index 9ff4a10..f3c807b 100644 --- a/src/bpm/main.go +++ b/src/bpm/main.go @@ -8,6 +8,7 @@ import ( "git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib" "log" "os" + "path" "path/filepath" "slices" "strings" @@ -62,6 +63,7 @@ const ( remove cleanup file + compile ) func getCommandType() commandType { @@ -86,6 +88,8 @@ func getCommandType() commandType { return cleanup case "file": return file + case "compile": + return compile default: return help } @@ -543,6 +547,51 @@ func resolveCommand() { } } } + case compile: + if len(subcommandArgs) == 0 { + fmt.Println("No source packages were given") + return + } + + // Read local databases + err := bpmlib.ReadLocalDatabases() + 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 current working directory + workdir, err := os.Getwd() + if err != nil { + log.Fatalf("Error: could not get working directory: %s", err) + } + + outputFilename := fmt.Sprintf(path.Join(workdir, "%s-%s-%d.bpm"), bpmpkg.PkgInfo.Name, bpmpkg.PkgInfo.Version, bpmpkg.PkgInfo.Revision) + + err = bpmlib.CompileSourcePackage(sourcePackage, outputFilename) + if err != nil { + log.Fatalf("Error: could not compile source package (%s): %s", sourcePackage, err) + } + + fmt.Printf("Package (%s) was successfully compiled! Binary package generated at: %s\n", sourcePackage, outputFilename) + } + default: printHelp() } diff --git a/src/bpmlib/compilation.go b/src/bpmlib/compilation.go new file mode 100644 index 0000000..be677e4 --- /dev/null +++ b/src/bpmlib/compilation.go @@ -0,0 +1,185 @@ +package bpmlib + +import ( + "errors" + "fmt" + "gopkg.in/yaml.v3" + "io" + "os" + "os/exec" + "path" + "strconv" +) + +func CompileSourcePackage(archiveFilename, outputFilename string) (err error) { + // Read BPM archive + bpmpkg, err := ReadPackage(archiveFilename) + if err != nil { + return err + } + + // Ensure package type is 'source' + if bpmpkg.PkgInfo.Type != "source" { + return errors.New("cannot compile a non-source package") + } + + // Get HOME directory + homeDir, err := os.UserHomeDir() + if err != nil { + return err + } + + 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 err + } + } + + // Create temporary directory + err = os.MkdirAll(tempDirectory, 0755) + if err != nil { + return err + } + + // Extract source.sh file + content, err := readTarballContent(archiveFilename, "source.sh") + if err != nil { + return err + } + sourceFile, err := os.Create(path.Join(tempDirectory, "source.sh")) + if err != nil { + return err + } + _, err = io.Copy(sourceFile, content.tarReader) + if err != nil { + return err + } + err = sourceFile.Close() + if err != nil { + return err + } + err = content.file.Close() + if err != nil { + return err + } + + // Extract source files + err = extractTarballDirectory(archiveFilename, "source-files", tempDirectory) + if err != nil { + return err + } + + // Create source directory + err = os.Mkdir(path.Join(tempDirectory, "source"), 0755) + if err != nil { + return 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)) + env = append(env, "BPM_PKG_ARCH="+GetArch()) + + // 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\" && cd \"$BPM_SOURCE\" && set -e && prepare)\n"+ // Run prepare() function if it exists + "[[ $(type -t build) == \"function\" ]] && (echo \"Running build() function\" && cd \"$BPM_SOURCE\" && set -e && build)\n"+ // Run build() function if it exists + "[[ $(type -t check) == \"function\" ]] && (echo \"Running check() function\" && cd \"$BPM_SOURCE\" && set -e && check)\n"+ // Run check() function if it exists + "exit 0") + cmd.Dir = tempDirectory + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = env + err = cmd.Run() + if err != nil { + return err + } + + // 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 err + } + } + + // Create new 'output' directory + err = os.Mkdir(path.Join(tempDirectory, "output"), 0755) + if err != nil { + return err + } + + // Run bash command + cmd = exec.Command("bash", "-e", "-c", + "set -a\n"+ // Source and export functions and variables in source.sh script + ". \"${BPM_WORKDIR}\"/source.sh\n"+ + "set +a\n"+ + "(echo \"Running package() function\" && cd \"$BPM_SOURCE\" && fakeroot -s \"$BPM_WORKDIR\"/fakeroot_file package)\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 + err = cmd.Run() + if err != nil { + return 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 + err = cmd.Run() + if err != nil { + return fmt.Errorf("files.tar.gz archive could not be created: %s", err) + } + + // Copy pkgInfo struct and set package type to binary + pkgInfo := bpmpkg.PkgInfo + pkgInfo.Type = "binary" + + // Marshal package info + pkgInfoBytes, err := yaml.Marshal(pkgInfo) + if err != nil { + return err + } + pkgInfoBytes = append(pkgInfoBytes, '\n') + + // Create pkg.info file + err = os.WriteFile(path.Join(tempDirectory, "pkg.info"), pkgInfoBytes, 0644) + if err != nil { + return err + } + + // Create final BPM archive + cmd = exec.Command("bash", "-c", "tar -cf "+outputFilename+" --owner=0 --group=0 -C \"$BPM_WORKDIR\" pkg.info pkg.files ${PACKAGE_SCRIPTS[@]} files.tar.gz") + cmd.Dir = tempDirectory + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + currentDir, err := os.Getwd() + if err != nil { + return err + } + cmd.Env = append(env, "CURRENT_DIR="+currentDir) + err = cmd.Run() + if err != nil { + return fmt.Errorf("BPM archive could not be created: %s", err) + } + + return nil +} diff --git a/src/bpmlib/tarball.go b/src/bpmlib/tarball.go index 84a0008..a6ef764 100644 --- a/src/bpmlib/tarball.go +++ b/src/bpmlib/tarball.go @@ -5,6 +5,8 @@ import ( "errors" "io" "os" + "path" + "strings" ) type tarballFileReader struct { @@ -41,3 +43,65 @@ func readTarballContent(tarballPath, fileToExtract string) (*tarballFileReader, return nil, errors.New("could not file in tarball") } + +func extractTarballDirectory(tarballPath, directoryToExtract, workingDirectory string) (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 + } + 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 + } + + // Copy data to file + _, err = io.Copy(file, tr) + if err != nil { + return err + } + + // Close file + file.Close() + default: + continue + } + } + } + + return nil +}