mirror of
https://github.com/EnumeratedDev/bpm.git
synced 2026-09-16 02:26:12 +00:00
599 lines
16 KiB
Go
599 lines
16 KiB
Go
package bpmlib
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/drone/envsubst"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
var rootCompilationUID = "65534"
|
|
var rootCompilationGID = "65534"
|
|
|
|
func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks, keepCompilationFiles, verbose 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")
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Download files
|
|
err = downloadPackageFiles(bpmpkg.PkgInfo, tempDirectory, verbose)
|
|
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_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="+bpmpkg.PkgInfo.OutputArch)
|
|
env = append(env, CompilationBPMConfig.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)
|
|
}
|
|
|
|
// Create output directories for each package
|
|
for _, pkg := range packagesToCompile {
|
|
// Create new output directory
|
|
err = os.Mkdir(path.Join(tempDirectory, "output_"+pkg.Name), 0755)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Change output directory owner
|
|
err = os.Chown(path.Join(tempDirectory, "output_"+pkg.Name), uid, gid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Compile each package
|
|
for _, pkg := range packagesToCompile {
|
|
// Get package function name
|
|
packageFunctionName := "package"
|
|
if bpmpkg.PkgInfo.IsSplitPackage() {
|
|
packageFunctionName = "package_" + pkg.Name
|
|
}
|
|
|
|
// 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 = append(env, "BPM_OUTPUT="+path.Join(tempDirectory, "output_"+pkg.Name))
|
|
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", fmt.Sprintf("find %s -printf \"%%P\\n\" | fakeroot -i %s/fakeroot_file tar -czf files.tar.gz --no-recursion -C %s -T -", "output_"+pkg.Name, tempDirectory, "output_"+pkg.Name))
|
|
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
|
|
pkgInfo.Arch = pkg.OutputArch
|
|
pkgInfo.OutputArch = ""
|
|
|
|
// Remove split package and downloads fields
|
|
pkgInfo.SplitPackages = nil
|
|
pkgInfo.Downloads = 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
|
|
}
|
|
|
|
// Remove output directory
|
|
err = os.RemoveAll(path.Join(tempDirectory, "output_"+pkg.Name))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
outputBpmPackages[pkgInfo.Name] = outputFilename
|
|
}
|
|
|
|
// Delete temporary directory
|
|
if !keepCompilationFiles {
|
|
os.RemoveAll(tempDirectory)
|
|
}
|
|
|
|
return outputBpmPackages, nil
|
|
}
|
|
|
|
func downloadPackageFiles(pkgInfo *PackageInfo, tempDirectory string, verbose bool) error {
|
|
for i, download := range pkgInfo.Downloads {
|
|
// Replace variables
|
|
replaceVars := func(s string) string {
|
|
switch s {
|
|
case "BPM_PKG_VERSION":
|
|
return pkgInfo.Version
|
|
case "BPM_PKG_NAME":
|
|
return pkgInfo.Name
|
|
case "BPM_SOURCE":
|
|
return path.Join(tempDirectory, "source/")
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
downloadUrl, err := envsubst.Eval(strings.TrimSpace(download.Url), replaceVars)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
extractTo, err := envsubst.Eval(strings.TrimSpace(download.ExtractTo), replaceVars)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cloneTo, err := envsubst.Eval(strings.TrimSpace(download.CloneTo), replaceVars)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Make relative paths absolute
|
|
if extractTo != "" && extractTo[0] != '/' {
|
|
extractTo = path.Join(tempDirectory, extractTo)
|
|
}
|
|
// Make relative paths absolute
|
|
if cloneTo != "" && cloneTo[0] != '/' {
|
|
cloneTo = path.Join(tempDirectory, cloneTo)
|
|
}
|
|
|
|
switch download.Type {
|
|
case "", "file":
|
|
filepath := path.Join(tempDirectory, path.Base(downloadUrl))
|
|
if download.Filepath != "" && download.Filepath[0] != '/' {
|
|
filepath = path.Join(tempDirectory, download.Filepath)
|
|
}
|
|
|
|
err := downloadFile("Downloading file "+strconv.Itoa(i+1), downloadUrl, filepath, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if download.Checksum != "skip" {
|
|
f, err := os.Open(filepath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
h := sha256.New()
|
|
_, err = io.Copy(h, f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if hex.EncodeToString(h.Sum(nil)) != download.Checksum {
|
|
fmt.Printf("Downloaded file checksum: %x\n", h.Sum(nil))
|
|
return fmt.Errorf("downloaded file checksums did not match")
|
|
}
|
|
} else {
|
|
fmt.Println("Skipping checksum checking...")
|
|
}
|
|
|
|
if !download.NoExtract && (strings.Contains(filepath, ".tar") || strings.HasSuffix(filepath, ".tgz")) {
|
|
cmd := exec.Command("tar", "xf", filepath, "--strip-components="+strconv.Itoa(download.ExtractStripComponents))
|
|
if verbose {
|
|
cmd.Args[1] = "xvf"
|
|
}
|
|
cmd.Dir = tempDirectory
|
|
if extractTo != "" {
|
|
err := os.MkdirAll(extractTo, 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd.Args = append(cmd.Args, "-C", extractTo)
|
|
}
|
|
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if !download.NoExtract && strings.HasSuffix(filepath, ".zip") {
|
|
cmd := exec.Command("unzip", filepath)
|
|
if extractTo != "" {
|
|
err := os.MkdirAll(extractTo, 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd.Args = append(cmd.Args, "-d", extractTo)
|
|
} else {
|
|
err := os.Mkdir(path.Join(tempDirectory, strings.TrimSuffix(path.Base(filepath), ".zip")), 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.Args = append(cmd.Args, "-d", path.Join(tempDirectory, strings.TrimSuffix(path.Base(filepath), ".zip")))
|
|
}
|
|
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case "git":
|
|
// Replace variables in git branch
|
|
gitBranch := download.GitBranch
|
|
gitBranch, err := envsubst.Eval(gitBranch, func(s string) string {
|
|
switch s {
|
|
case "BPM_PKG_VERSION":
|
|
return pkgInfo.Version
|
|
case "BPM_PKG_NAME":
|
|
return pkgInfo.Name
|
|
default:
|
|
return ""
|
|
}
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := exec.Command("git", "clone", "--depth=1", downloadUrl)
|
|
cmd.Dir = tempDirectory
|
|
if gitBranch != "" {
|
|
cmd.Args = slices.Insert(cmd.Args, len(cmd.Args)-1, "--branch="+gitBranch)
|
|
}
|
|
if cloneTo != "" {
|
|
err := os.MkdirAll(cloneTo, 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd.Args = append(cmd.Args, cloneTo)
|
|
}
|
|
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if download.Checksum != "skip" {
|
|
cmd := exec.Command("git", "rev-parse", "HEAD")
|
|
if cloneTo != "" {
|
|
cmd.Dir = cloneTo
|
|
} else {
|
|
cmd.Dir = path.Join(tempDirectory, strings.TrimSuffix(path.Base(downloadUrl), ".git"))
|
|
}
|
|
|
|
branchChecksum, err := cmd.Output()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if strings.TrimSpace(string(branchChecksum)) != download.Checksum {
|
|
fmt.Printf("Git branch checksum: %s\n", strings.TrimSpace(string(branchChecksum)))
|
|
return fmt.Errorf("Cloned git repository checksum did not match")
|
|
}
|
|
} else {
|
|
fmt.Println("Skipping checksum checking...")
|
|
}
|
|
default:
|
|
return fmt.Errorf("unknown download type (%s)", download.Type)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func showPackageFiles(archiveFilename string) error {
|
|
// Read BPM archive
|
|
bpmpkg, err := ReadPackage(archiveFilename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ensure package type is 'source'
|
|
if bpmpkg.PkgInfo.Type != "source" {
|
|
return fmt.Errorf("cannot compile a non-source package")
|
|
}
|
|
|
|
printTarballContent := func(filename string) error {
|
|
// Get tarball reader for file
|
|
reader, err := readTarballFile(archiveFilename, filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Read file content
|
|
data, err := io.ReadAll(reader.tarReader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Print file name and content
|
|
fmt.Println("==============================")
|
|
fmt.Printf("%s: %s\n", bpmpkg.PkgInfo.Name, filename)
|
|
fmt.Println("==============================")
|
|
fmt.Printf("%s\n", data)
|
|
|
|
// Close reader
|
|
err = reader.file.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Print pkg.info content
|
|
err = printTarballContent("pkg.info")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Print source files contents
|
|
files, err := listTarballContent(archiveFilename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, file := range files {
|
|
if !strings.HasPrefix(file, "source-files/") {
|
|
continue
|
|
}
|
|
|
|
// Print source file content
|
|
err = printTarballContent(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Print source.sh content
|
|
err = printTarballContent("source.sh")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|