Compare commits

..

No commits in common. "b64650e11499b5899e13a5c870e48dc11df5c0bf" and "b867612961d820cca61da8d284619436868d582f" have entirely different histories.

10 changed files with 243 additions and 934 deletions

3
.gitignore vendored
View file

@ -1,4 +1 @@
.idea .idea
.vscode
/bin
*.epk

View file

@ -3,34 +3,3 @@
Eternity is the build tool for eon: It builds packages at speeds RPMBUILD wishes it could. Eternity is the build tool for eon: It builds packages at speeds RPMBUILD wishes it could.
Currently not suitable for production use. Currently not suitable for production use.
## Dependencies
To use eternity, you must install:
go 1.23^
fakeroot-tcp 1.36^
## Quick Start
To simply build and install on your machine, run:
```
./build.sh
```
## Building
To build the cli, run:
```
mkdir bin
CGO_ENABLED=0 go build -ldflags "-s -w -v" -o ./bin/eternity ./cmd/main.go
```
## Installation
To install your eternity CLI after building it, run:
```
sudo install ./bin/eternity /bin
```

View file

@ -1,13 +0,0 @@
#!/bin/sh
# Create the bin directory
mkdir -p bin
# Build the CLI
echo "Building the CLI..."
CGO_ENABLED=0 go build -ldflags "-s -w" -o ./bin/eternity ./cmd/main.go
# Install the CLI
echo "Installing the CLI..."
sudo install ./bin/eternity /bin
echo "CLI installed successfully!"

BIN
cmd/cmd Executable file

Binary file not shown.

View file

@ -1,19 +1,12 @@
package main package main
import ( import (
"eternity/common"
"eternity/lib"
"fmt" "fmt"
"os" "os"
"regexp"
"strings"
"git.oreonproject.org/oreonproject/eternity/common"
"git.oreonproject.org/oreonproject/eternity/lib"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"github.com/Masterminds/semver"
"github.com/sassoftware/go-rpmutils"
) )
func main() { func main() {
@ -41,31 +34,6 @@ func main() {
} }
} }
// Allow the user to choose an output dir
var outputDir = "./" // Default output directory
// Loop through arguments to find "-o" or "--output"
for i := 2; i < len(os.Args); i++ {
if os.Args[i] == "-o" || os.Args[i] == "--output" {
// Check if next argument exists and is not another flag
if i+1 < len(os.Args) && !strings.HasPrefix(os.Args[i+1], "-") {
outputDir = os.Args[i+1]
if !filepath.IsAbs(outputDir) {
outputDir, _ = filepath.Abs(outputDir) // Convert to absolute path if relative
}
break
} else {
fmt.Println("FATAL: Output directory not specified or invalid after", os.Args[i])
os.Exit(1)
}
}
}
logger.LogFunc(lib.Log{
Level: "INFO",
Content: "Build directory set to: " + outputDir,
})
privateKey, err, vagueError := common.LoadPrivateKey(logger) privateKey, err, vagueError := common.LoadPrivateKey(logger)
if err != nil || vagueError != nil { if err != nil || vagueError != nil {
common.LoadPrivateKeyHandleError(err, vagueError, logger) common.LoadPrivateKeyHandleError(err, vagueError, logger)
@ -87,13 +55,11 @@ func main() {
os.Exit(1) os.Exit(1)
} }
}() }()
var size int64 tempDir, err, vagueError = lib.BuildEPK("./", inMemory, config.Build, logger)
size, tempDir, err, vagueError = lib.BuildEPK("./", inMemory, config.Build, logger)
if err != nil || vagueError != nil { if err != nil || vagueError != nil {
common.BuildEPKHandleError(tempDir, err, vagueError, logger) common.BuildEPKHandleError(tempDir, err, vagueError, logger)
} else { } else {
filePath := filepath.Join(outputDir, config.Metadata.Name+"-"+config.Metadata.Version.String()) filePath := filepath.Join("./", config.Metadata.Name+"-"+config.Metadata.Version.String())
config.Metadata.DecompressedSize = size
err, vagueError := lib.PackageEPK(config.Metadata, config.Build, tempDir, filePath+".epk", privateKey, logger) err, vagueError := lib.PackageEPK(config.Metadata, config.Build, tempDir, filePath+".epk", privateKey, logger)
if err != nil || vagueError != nil { if err != nil || vagueError != nil {
common.PackageEPKHandleError(err, vagueError, logger) common.PackageEPKHandleError(err, vagueError, logger)
@ -106,201 +72,6 @@ func main() {
} }
} }
} }
case "repo":
if len(os.Args) < 4 {
fmt.Println("Usage: eternity repo <build/generate> <directory>")
fmt.Println("See 'eternity help repo' for more information.")
os.Exit(1)
}
privateKey, err, vagueError := common.LoadPrivateKey(logger)
if err != nil || vagueError != nil {
common.LoadPrivateKeyHandleError(err, vagueError, logger)
}
switch os.Args[2] {
case "generate":
err, vagueError := lib.GenerateRepository(os.Args[3], privateKey, logger)
if err != nil || vagueError != nil {
common.GenerateRepositoryHandleError(err, vagueError, logger)
}
}
case "convert":
if len(os.Args) < 4 {
fmt.Println("Usage: eternity convert </path/to/rpm> [</path/to/output/epk>]")
os.Exit(1)
}
privateKey, err, vagueError := common.LoadPrivateKey(logger)
if err != nil || vagueError != nil {
common.LoadPrivateKeyHandleError(err, vagueError, logger)
}
rpmFile, err := os.Open(os.Args[2])
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error opening RPM: " + err.Error(),
})
}
rpm, err := rpmutils.ReadRpm(rpmFile)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error reading RPM: " + err.Error(),
})
}
name, err := rpm.Header.GetString(rpmutils.NAME)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error getting RPM name: " + err.Error(),
})
}
description, err := rpm.Header.GetString(rpmutils.DESCRIPTION)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error getting RPM description: " + err.Error(),
})
}
longDescription := logger.LogFunc(lib.Log{
Level: "INFO",
Content: "Enter a long description for the package: ",
Prompt: true,
})
version, err := rpm.Header.GetString(rpmutils.VERSION)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error getting RPM version: " + err.Error(),
})
}
release, err := rpm.Header.GetString(rpmutils.RELEASE)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error getting RPM release: " + err.Error(),
})
}
semanticVersion, err := semver.NewVersion(version + "-" + release)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error parsing RPM version and release: " + err.Error(),
})
}
author := logger.LogFunc(lib.Log{
Level: "PROMPT",
Content: "Enter your preferred author name. This must be the same as the author name used in " +
"eternity.json and associated with your keypair, otherwise it will cause issues with EPK" +
" verification and your repository will be rejected by Eon and cannot be trusted.",
Prompt: true,
})
license, err := rpm.Header.GetString(rpmutils.LICENSE)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error getting RPM license: " + err.Error(),
})
}
decompressedSize, err := rpm.Header.GetUint64s(rpmutils.SIZE)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error getting RPM size: " + err.Error(),
})
}
dependenciesRpm, err := rpm.Header.GetStrings(rpmutils.REQUIRENAME)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error getting RPM dependencies: " + err.Error(),
})
}
libRegex := regexp.MustCompile(`^([a-z]+)`)
var dependencies []string
for _, dependency := range dependenciesRpm {
dependencyMatch := libRegex.FindStringSubmatch(dependency)
if len(dependencyMatch) > 1 {
match := dependencyMatch[1]
if match != "rpmlib" && match != "rtld" {
var exists bool
for _, dep := range dependencies {
if dep == match {
exists = true
}
}
if !exists {
dependencies = append(dependencies, match)
}
}
}
}
fmt.Println(dependencies)
architecture, err := rpm.Header.GetString(rpmutils.ARCH)
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error getting RPM architecture: " + err.Error(),
})
}
metadata := lib.Metadata{
Name: name,
Description: description,
LongDescription: longDescription,
Version: semanticVersion,
Author: author,
License: license,
Architecture: architecture,
Dependencies: dependencies,
DecompressedSize: int64(decompressedSize[0]),
SpecialFiles: lib.SpecialFiles{},
}
// Output the RPM data to a temporary directory
tempDir, err := os.MkdirTemp("", "eternity-convert-")
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error creating temporary directory: " + err.Error(),
})
}
err = rpm.ExpandPayload(filepath.Join(tempDir, "payload"))
if err != nil {
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Error expanding RPM payload: " + err.Error(),
})
}
// Package the EPK
err, vagueError = lib.PackageEPK(metadata, lib.Build{
TargetRoot: "payload",
}, tempDir, os.Args[3], privateKey, logger)
if err != nil || vagueError != nil {
common.PackageEPKHandleError(err, vagueError, logger)
} else {
logger.LogFunc(lib.Log{
Level: "INFO",
Content: "Successfully converted RPM to EPK at " + os.Args[3],
})
}
case "help": case "help":
if len(os.Args) > 2 { if len(os.Args) > 2 {
switch os.Args[2] { switch os.Args[2] {
@ -308,7 +79,7 @@ func main() {
fmt.Println("Usage: eternity build [-d/--disk]") fmt.Println("Usage: eternity build [-d/--disk]")
fmt.Println(" -d, --disk Build EPK on disk instead of in memory. This is useful for large EPKs.") fmt.Println(" -d, --disk Build EPK on disk instead of in memory. This is useful for large EPKs.")
case "convert": case "convert":
fmt.Println("Usage: eternity convert </path/to/rpm> </path/to/output/epk>") fmt.Println("Usage: eternity convert </path/to/rpm> [</path/to/output/epk>]")
case "repo": case "repo":
fmt.Println("Usage: eternity repo <build/generate> <directory>") fmt.Println("Usage: eternity repo <build/generate> <directory>")
fmt.Println(" build Build a repository from a directory containing EPK project directories.") fmt.Println(" build Build a repository from a directory containing EPK project directories.")

View file

@ -1,18 +1,15 @@
package common package common
import ( import (
"bufio"
"errors"
"fmt"
"os"
"strings"
"git.oreonproject.org/oreonproject/eternity/lib"
"crypto/ed25519" "crypto/ed25519"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors"
"eternity/lib"
"fmt"
"os"
"path/filepath" "path/filepath"
"strings"
"github.com/fatih/color" "github.com/fatih/color"
) )
@ -38,20 +35,18 @@ var DefaultLogger = lib.Logger{
fmt.Println(severityPretty, log.Content) fmt.Println(severityPretty, log.Content)
if log.Prompt { if log.Prompt {
fmt.Print(": ") fmt.Print(": ")
reader := bufio.NewReader(os.Stdin) var userInput string
userInput, err := reader.ReadString('\n') _, err := fmt.Scanln(&userInput)
if err != nil { if err != nil {
fmt.Println(color.RedString("[FATAL]"), "Failed to read user input:", err) fmt.Println(color.RedString("[FATAL]"), "Failed to read user input:", err)
os.Exit(1) os.Exit(1)
} else { } else {
return userInput[:len(userInput)-1] return userInput
} }
} }
return "" return ""
}, },
PromptSupported: true, PromptSupported: true,
StdoutSupported: true,
Stdout: os.Stdout,
} }
// EternityJsonHandleError handles errors related to the parsing of eternity.json // EternityJsonHandleError handles errors related to the parsing of eternity.json
@ -159,11 +154,6 @@ func BuildEPKHandleError(tempDir string, err error, vagueError error, logger *li
Level: "FATAL", Level: "FATAL",
Content: "Failed to count files in target root: " + err.Error(), Content: "Failed to count files in target root: " + err.Error(),
}) })
case errors.Is(vagueError, lib.ErrBuildEPKBadBuildType):
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Invalid build type: " + err.Error(),
})
} }
} }
@ -392,50 +382,3 @@ func LoadPrivateKeyHandleError(err error, vagueError error, logger *lib.Logger)
}) })
} }
} }
func GenerateRepositoryHandleError(err error, vagueError error, logger *lib.Logger) {
switch {
case errors.Is(vagueError, lib.ErrGenerateRepositoryStatError):
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Failed to stat directory: " + err.Error(),
Prompt: false,
})
case errors.Is(vagueError, lib.ErrGenerateRepositoryNotDirectory):
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Specified path is not a directory",
Prompt: false,
})
case errors.Is(vagueError, lib.ErrGenerateRepositoryFailedToWalk):
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Failed to walk directory: " + err.Error(),
Prompt: false,
})
case errors.Is(vagueError, lib.ErrGenerateRepositoryCannotMarshalJSON):
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Failed to marshal JSON: " + err.Error(),
Prompt: false,
})
case errors.Is(vagueError, lib.ErrGenerateRepositoryCannotOpenFile):
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Failed to open file for writing: " + err.Error(),
Prompt: false,
})
case errors.Is(vagueError, lib.ErrGenerateRepositoryCannotWriteFile):
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Failed to write to file: " + err.Error(),
Prompt: false,
})
case errors.Is(vagueError, lib.ErrGenerateRepositoryCannotCloseFile):
logger.LogFunc(lib.Log{
Level: "FATAL",
Content: "Failed to close file: " + err.Error(),
Prompt: false,
})
}
}

View file

@ -8,3 +8,4 @@
</magic> </magic>
</mime-type> </mime-type>
</mime-info> </mime-info>

11
go.mod
View file

@ -1,4 +1,4 @@
module git.oreonproject.org/oreonproject/eternity module eternity
go 1.23 go 1.23
@ -7,17 +7,10 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 github.com/cespare/xxhash/v2 v2.3.0
github.com/fatih/color v1.17.0 github.com/fatih/color v1.17.0
github.com/klauspost/compress v1.17.9 github.com/klauspost/compress v1.17.9
github.com/sassoftware/go-rpmutils v0.4.0
) )
require ( require (
github.com/DataDog/zstd v1.5.6 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/cloudflare/circl v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect golang.org/x/sys v0.24.0 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/sys v0.25.0 // indirect
) )

76
go.sum
View file

@ -1,23 +1,7 @@
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY=
github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI=
github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
@ -27,67 +11,7 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg=
github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -1,19 +1,18 @@
package lib package lib
import ( import (
"bytes"
"errors"
"io"
"os"
"strconv"
"strings"
"archive/tar" "archive/tar"
"bytes"
"crypto/ed25519" "crypto/ed25519"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"errors"
"io"
"os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strconv"
"strings"
"github.com/Masterminds/semver" "github.com/Masterminds/semver"
"github.com/cespare/xxhash/v2" "github.com/cespare/xxhash/v2"
@ -22,40 +21,43 @@ import (
// SpecialFiles is a struct that contains the special files that are not to be deleted or replaced // SpecialFiles is a struct that contains the special files that are not to be deleted or replaced
type SpecialFiles struct { type SpecialFiles struct {
NoDelete []string `json:"noDelete"` NoDelete []string
NoReplace []string `json:"noReplace"` NoReplace []string
} }
// Metadata is a struct that contains the metadata of the package // Metadata is a struct that contains the metadata of the package
type Metadata struct { type Metadata struct {
Name string `json:"name"` Name string
Description string `json:"desc"` Description string
LongDescription string `json:"longDesc"` LongDescription string
Version *semver.Version Version semver.Version
VersionString string `json:"version"` Author string
Author string `json:"author"` License string
License string `json:"license"` Architecture string
Architecture string `json:"arch"` Dependencies []string
// The decompressed size may be larger than the int64 allocated for a compressed file SpecialFiles SpecialFiles
DecompressedSize int64
Dependencies []string `json:"deps"`
SpecialFiles SpecialFiles `json:"specialFiles"`
} }
// Build is a struct that contains the build configuration of the package // Build is a struct that contains the build configuration of the package
type Build struct { type Build struct {
Type string `json:"type"` Type string
Dependencies []string `json:"deps"` Dependencies []string
Steps []string `json:"steps"` Steps []string
TargetRoot string `json:"root"` TargetRoot string
HooksFolder string `json:"hooks"` HooksFolder PotentiallyNullString
FilesFolder string `json:"files"` FilesFolder PotentiallyNullString
}
// PotentiallyNullString is a struct that contains a string that may be null
type PotentiallyNullString struct {
Value string
Null bool
} }
// Config is a struct that contains the configuration of the package // Config is a struct that contains the configuration of the package
type Config struct { type Config struct {
Metadata Metadata `json:"metadata"` Metadata Metadata
Build Build `json:"build"` Build Build
} }
// Log is a struct that contains the log information // Log is a struct that contains the log information
@ -69,8 +71,6 @@ type Log struct {
type Logger struct { type Logger struct {
LogFunc func(Log) string LogFunc func(Log) string
PromptSupported bool PromptSupported bool
StdoutSupported bool
Stdout io.Writer
} }
var ErrEternityJsonOpenError = errors.New("error opening eternity.json") var ErrEternityJsonOpenError = errors.New("error opening eternity.json")
@ -78,6 +78,22 @@ var ErrEternityJsonReadError = errors.New("error reading eternity.json")
var ErrEternityJsonParseError = errors.New("error parsing eternity.json") var ErrEternityJsonParseError = errors.New("error parsing eternity.json")
var ErrEternityJsonMapError = errors.New("error mapping eternity.json") var ErrEternityJsonMapError = errors.New("error mapping eternity.json")
// interfaceToStringSlice converts an interface slice to a string slice
func interfaceToStringSlice(interfaceSlice []interface{}, interfaceName string) ([]string, error) {
// Yes, it's meant to be empty and not nil: JSON arrays are empty, not nil
//goland:noinspection GoPreferNilSlice
stringSlice := []string{}
for _, interfaceValue := range interfaceSlice {
stringValue, ok := interfaceValue.(string)
if !ok {
return nil, errors.New(interfaceName + " are not strings")
}
stringSlice = append(stringSlice, stringValue)
}
return stringSlice, nil
}
// ParseConfig parses the eternity.json file // ParseConfig parses the eternity.json file
func ParseConfig(path string, logger *Logger) (Config, error, error) { func ParseConfig(path string, logger *Logger) (Config, error, error) {
// Open eternity.json // Open eternity.json
@ -91,22 +107,143 @@ func ParseConfig(path string, logger *Logger) (Config, error, error) {
return Config{}, err, ErrEternityJsonOpenError return Config{}, err, ErrEternityJsonOpenError
} }
// Convert the file to a byte buffer
var fileBytes bytes.Buffer
_, err = io.Copy(&fileBytes, file)
if err != nil {
return Config{}, err, ErrEternityJsonReadError
}
// Parse the file as JSON // Parse the file as JSON
var config Config var config map[string]interface{}
decoder := json.NewDecoder(file) err = json.Unmarshal(fileBytes.Bytes(), &config)
err = decoder.Decode(&config)
if err != nil { if err != nil {
return Config{}, err, ErrEternityJsonParseError return Config{}, err, ErrEternityJsonParseError
} }
// Map the JSON version to a semver version // Map SpecialFiles
config.Metadata.Version, err = semver.NewVersion(config.Metadata.VersionString) var parsedSpecialFiles SpecialFiles
specialFiles, ok := config["specialFiles"].(map[string]interface{})
if !ok {
return Config{}, errors.New("specialFiles is not an object"), ErrEternityJsonMapError
}
noDelete, ok := specialFiles["noDelete"].([]interface{})
if !ok {
return Config{}, errors.New("noDelete is not an array"), ErrEternityJsonMapError
}
parsedSpecialFiles.NoDelete, err = interfaceToStringSlice(noDelete, "noDelete")
if err != nil {
return Config{}, err, ErrEternityJsonMapError
}
noReplace, ok := specialFiles["noReplace"].([]interface{})
if !ok {
return Config{}, errors.New("noReplace is not an array"), ErrEternityJsonMapError
}
parsedSpecialFiles.NoReplace, err = interfaceToStringSlice(noReplace, "noReplace")
if err != nil { if err != nil {
return Config{}, err, ErrEternityJsonMapError return Config{}, err, ErrEternityJsonMapError
} }
// Declare the parsedMetadata object
var parsedMetadata Metadata
// Append parsedSpecialFiles to parsedMetadata
parsedMetadata.SpecialFiles = parsedSpecialFiles
// Map the metadata
parsedMetadata.Name, ok = config["name"].(string)
if !ok {
return Config{}, errors.New("name is not a string"), ErrEternityJsonMapError
}
parsedMetadata.Description, ok = config["desc"].(string)
if !ok {
return Config{}, errors.New("description is not a string"), ErrEternityJsonMapError
}
parsedMetadata.LongDescription, ok = config["longDesc"].(string)
if !ok {
return Config{}, errors.New("longDesc is not a string"), ErrEternityJsonMapError
}
versionString, ok := config["version"].(string)
if !ok {
return Config{}, errors.New("version is not a string"), ErrEternityJsonMapError
}
versionPointer, err := semver.NewVersion(versionString)
if err != nil {
return Config{}, err, ErrEternityJsonMapError
}
parsedMetadata.Version = *versionPointer
parsedMetadata.Author, ok = config["author"].(string)
if !ok {
return Config{}, errors.New("author is not a string"), ErrEternityJsonMapError
}
parsedMetadata.License, ok = config["license"].(string)
if !ok {
return Config{}, errors.New("license is not a string"), ErrEternityJsonMapError
}
parsedMetadata.Architecture, ok = config["arch"].(string)
if !ok {
return Config{}, errors.New("arch is not a string"), ErrEternityJsonMapError
}
dependencies, ok := config["deps"].([]interface{})
if !ok {
return Config{}, errors.New("deps is not an array"), ErrEternityJsonMapError
}
parsedMetadata.Dependencies, err = interfaceToStringSlice(dependencies, "dependencies")
if err != nil {
return Config{}, err, ErrEternityJsonMapError
}
// Map build
var parsedBuild Build
build, ok := config["build"].(map[string]interface{})
if !ok {
return Config{}, errors.New("build is not an object"), ErrEternityJsonMapError
}
parsedBuild.Type, ok = build["type"].(string)
if !ok {
return Config{}, errors.New("type is not a string"), ErrEternityJsonMapError
}
buildDependencies, ok := build["deps"].([]interface{})
if !ok {
return Config{}, errors.New("deps is not an array"), ErrEternityJsonMapError
}
parsedBuild.Dependencies, err = interfaceToStringSlice(buildDependencies, "deps")
if err != nil {
return Config{}, err, ErrEternityJsonMapError
}
steps, ok := build["steps"].([]interface{})
if !ok {
return Config{}, errors.New("steps is not an array"), ErrEternityJsonMapError
}
parsedBuild.Steps, err = interfaceToStringSlice(steps, "steps")
if err != nil {
return Config{}, err, ErrEternityJsonMapError
}
parsedBuild.TargetRoot, ok = build["root"].(string)
if !ok {
return Config{}, errors.New("root is not a string"), ErrEternityJsonMapError
}
hooksFolder, ok := build["hooks"].(string)
if !ok {
parsedBuild.HooksFolder = PotentiallyNullString{Null: true}
} else {
parsedBuild.HooksFolder = PotentiallyNullString{Null: false, Value: hooksFolder}
}
filesFolder, ok := build["files"].(string)
if !ok {
parsedBuild.FilesFolder = PotentiallyNullString{Null: true}
} else {
parsedBuild.FilesFolder = PotentiallyNullString{Null: false, Value: filesFolder}
}
// Create the final Config object
parsedConfig := Config{
Metadata: parsedMetadata,
Build: parsedBuild,
}
// Return the final Config object // Return the final Config object
return config, nil, nil return parsedConfig, nil, nil
} }
var ErrBuildEPKTemporaryDirectoryError = errors.New("error creating temporary directory") var ErrBuildEPKTemporaryDirectoryError = errors.New("error creating temporary directory")
@ -119,66 +256,48 @@ var ErrBuildEPKWritingBuildShError = errors.New("error writing to build.sh")
var ErrBuildEPKTargetRootError = errors.New("error creating target root") var ErrBuildEPKTargetRootError = errors.New("error creating target root")
var ErrBuildEPKExecutingBuildShError = errors.New("error executing build.sh") var ErrBuildEPKExecutingBuildShError = errors.New("error executing build.sh")
var ErrBuildEPKCountingFilesError = errors.New("error counting files") var ErrBuildEPKCountingFilesError = errors.New("error counting files")
var ErrBuildEPKBadBuildType = errors.New("bad build type")
var ErrBuildEPKDirectoryDoesNotExist = errors.New("required directory does not exist")
var ErrBubbleWrapInsufficientPermissions = errors.New("bwrap does not have the necessary permissions")
// BuildEPK builds the EPK package into a build directory // BuildEPK builds the EPK package into a build directory
func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logger) (int64, string, error, error) { func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logger) (string, error, error) {
var tempDir string var tempDir string
switch buildConfig.Type { switch buildConfig.Type {
case "chroot": case "chroot":
return 0, "", nil, ErrBuildEPKChrootError return "", nil, ErrBuildEPKChrootError
case "unrestricted": case "unrestricted":
return 0, "", nil, ErrBuildEPKUnrestrictedError return "", nil, ErrBuildEPKUnrestrictedError
case "host": case "host":
// Set up the temp dir // Set up the temp dir
var err error var err error
if inMemory { if inMemory {
// Builds in /tmp. This means that the program must fit in RAM. Luckily, most programs do. // Builds in /tmp. This means that the program must fit in RAM. Luckily, most programs do.
// If you're building a large program, you might want to consider using a disk build. // If you're building a large program, you might want to consider using a disk build.
logger.LogFunc(Log{
Level: "INFO",
Content: "Creating temp directory",
Prompt: false,
})
tempDir, err = os.MkdirTemp("/tmp", "eternity-build-") tempDir, err = os.MkdirTemp("/tmp", "eternity-build-")
} else { } else {
// Builds on disk. This is slower but if your program can't fit in RAM, you're out of luck. // Builds on disk. This is slower but if your program can't fit in RAM, you're out of luck.
// If your program can fit in RAM, you might want to consider using an in-memory build. // If your program can fit in RAM, you might want to consider using an in-memory build.
logger.LogFunc(Log{
Level: "INFO",
Content: "Creating temp directory on disk",
Prompt: false,
})
tempDir, err = os.MkdirTemp(projectDir, "eternity-build-") tempDir, err = os.MkdirTemp(projectDir, "eternity-build-")
} }
if err != nil { if err != nil {
logger.LogFunc(Log{ return tempDir, err, ErrBuildEPKTemporaryDirectoryError
Level: "INFO",
Content: "Failed to create temporary directory, returning the error",
Prompt: false,
})
return 0, tempDir, err, ErrBuildEPKTemporaryDirectoryError
} }
// Copy the hooks folder // Copy the hooks folder
if buildConfig.HooksFolder != "" { if buildConfig.HooksFolder.Null != true {
hooksDir := filepath.Join(projectDir, buildConfig.HooksFolder) hooksDir := filepath.Join(projectDir, buildConfig.HooksFolder.Value)
targetHooksDir := filepath.Join(tempDir, buildConfig.HooksFolder) targetHooksDir := filepath.Join(tempDir, buildConfig.HooksFolder.Value)
logger.LogFunc(Log{ logger.LogFunc(Log{
Level: "INFO", Content: "Copying hooks from " + hooksDir + " to " + targetHooksDir, Prompt: false, Level: "INFO", Content: "Copying hooks from " + hooksDir + " to " + targetHooksDir, Prompt: false,
}) })
err = os.MkdirAll(targetHooksDir, 0755) err = os.MkdirAll(targetHooksDir, 0755)
if err != nil { if err != nil {
return 0, tempDir, err, ErrBuildEPKCreateHooksError return tempDir, err, ErrBuildEPKCreateHooksError
} }
err = os.CopyFS(targetHooksDir, os.DirFS(hooksDir)) err = os.CopyFS(targetHooksDir, os.DirFS(hooksDir))
if err != nil { if err != nil {
return 0, tempDir, err, ErrBuildEPKCopyHooksError return tempDir, err, ErrBuildEPKCopyHooksError
} }
} }
@ -195,19 +314,19 @@ func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logge
file, err := os.OpenFile(tempDir+"/build.sh", os.O_CREATE|os.O_RDWR, 0755) file, err := os.OpenFile(tempDir+"/build.sh", os.O_CREATE|os.O_RDWR, 0755)
if err != nil { if err != nil {
return 0, tempDir, err, ErrBuildEPKBuildShError return tempDir, err, ErrBuildEPKBuildShError
} }
_, err = file.WriteString(shellScript) _, err = file.WriteString(shellScript)
if err != nil { if err != nil {
return 0, tempDir, err, ErrBuildEPKWritingBuildShError return tempDir, err, ErrBuildEPKWritingBuildShError
} }
// Set up the target root // Set up the target root
targetRoot := filepath.Join(tempDir, buildConfig.TargetRoot) targetRoot := filepath.Join(tempDir, buildConfig.TargetRoot)
err = os.MkdirAll(targetRoot, 0755) err = os.MkdirAll(targetRoot, 0755)
if err != nil { if err != nil {
return 0, tempDir, err, ErrBuildEPKTargetRootError return tempDir, err, ErrBuildEPKTargetRootError
} }
// Execute the shell script in BWrap // Execute the shell script in BWrap
@ -219,62 +338,6 @@ func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logge
// copy-pasted most of the host files into the container, then disabled networking. This also allows us to use // copy-pasted most of the host files into the container, then disabled networking. This also allows us to use
// fakeroot and minimises the blast radius of a malicious package (hopefully) by not allowing the home directory // fakeroot and minimises the blast radius of a malicious package (hopefully) by not allowing the home directory
// or any files owned by root to be viewed or modified (too bad if you've got sensitive data in /var or /etc :P) // or any files owned by root to be viewed or modified (too bad if you've got sensitive data in /var or /etc :P)
// Ensure all directories being bound exist
directoriesToCheck := []string{"/bin", "/lib", "/lib64", "/usr", "/etc", "/var", "/sys", "/opt", targetRoot, tempDir}
for _, dir := range directoriesToCheck {
if _, err := os.Stat(dir); os.IsNotExist(err) {
logger.LogFunc(Log{
Level: "ERROR",
Content: "Directory does not exist: " + dir,
Prompt: false,
})
return 0, tempDir, err, ErrBuildEPKDirectoryDoesNotExist
}
}
// ensure fakeroot-tcp is installed at /usr/bin/fakeroot-tcp
// if _, err := os.Stat("/usr/bin/fakeroot-tcp"); os.IsNotExist(err) {
// logger.LogFunc(Log{
// Level: "FATAL",
// Content: "fakeroot-tcp is not installed",
// Prompt: false,
// })
// return 0, tempDir, err, errors.New("fakeroot-tcp is not installed")
// }
// Check if bwrap is available and has the necessary permissions
logger.LogFunc(Log{
Level: "INFO",
Content: "Checking if bwrap is available and has the necessary permissions",
Prompt: false,
})
cmd := exec.Command("bwrap", "--version")
err = cmd.Run()
if err != nil {
logger.LogFunc(Log{
Level: "ERROR",
Content: "bwrap is not available or does not have the necessary permissions",
Prompt: false,
})
return 0, tempDir, err, ErrBubbleWrapInsufficientPermissions
}
logger.LogFunc(Log{
Level: "DEBUG",
Content: "Temp directory: " + tempDir,
Prompt: false,
})
logger.LogFunc(Log{
Level: "DEBUG",
Content: "Target root: " + targetRoot,
Prompt: false,
})
logger.LogFunc(Log{
Level: "DEBUG",
Content: "Joined path: " + filepath.Join("/", buildConfig.TargetRoot),
Prompt: false,
})
arguments := []string{ arguments := []string{
"--unshare-net", "--unshare-net",
"--bind", "/bin", "/bin", "--bind", "/bin", "/bin",
@ -291,75 +354,26 @@ func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logge
"--tmpfs", "/run", "--tmpfs", "/run",
"--tmpfs", "/tmp", "--tmpfs", "/tmp",
"--proc", "/proc", "--proc", "/proc",
"/usr/bin/fakeroot-tcp", "--", // fakeroot-tcp still installs as "fakeroot" for some reason TODO: handle wrong fakeroot installation "/usr/bin/fakeroot-tcp", "--",
"/bin/sh", "/eternity/build.sh", "/bin/sh", "/eternity/build.sh",
} }
if buildConfig.FilesFolder != "" { if buildConfig.FilesFolder.Null != true {
logger.LogFunc(Log{
Level: "INFO",
Content: "Binding files folder to container",
Prompt: false,
})
arguments = arguments[:len(arguments)-4] arguments = arguments[:len(arguments)-4]
arguments = append( arguments = append(
arguments, "--bind", filepath.Join(projectDir, buildConfig.FilesFolder), filepath.Join("/", buildConfig.FilesFolder), arguments, "--bind", filepath.Join(projectDir, buildConfig.FilesFolder.Value), filepath.Join("/", buildConfig.FilesFolder.Value),
"/usr/bin/fakeroot-tcp", "--", "/usr/bin/fakeroot-tcp", "--",
"/bin/sh", "/eternity/build.sh", "/bin/sh", "/eternity/build.sh",
) )
} }
logger.LogFunc(Log{ err = exec.Command("bwrap", arguments...).Run()
Level: "INFO",
Content: "Executing build script in container",
Prompt: false,
})
logger.LogFunc(Log{
Level: "DEBUG",
Content: "Command: bwrap " + strings.Join(arguments, " "),
Prompt: false,
})
cmd = exec.Command("bwrap", arguments...)
if logger.StdoutSupported {
cmd.Stdout = logger.Stdout
}
var stdoutBuf, stderrBuf bytes.Buffer
cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf
err = cmd.Run()
stdoutStr := stdoutBuf.String()
stderrStr := stderrBuf.String()
if logger.StdoutSupported {
logger.Stdout.Write([]byte(stdoutStr))
logger.Stdout.Write([]byte(stderrStr))
}
logger.LogFunc(Log{
Level: "INFO",
Content: "Command stdout: " + stdoutStr,
Prompt: false,
})
logger.LogFunc(Log{
Level: "INFO",
Content: "Command stderr: " + stderrStr,
Prompt: false,
})
if err != nil { if err != nil {
logger.LogFunc(Log{ return tempDir, err, ErrBuildEPKExecutingBuildShError
Level: "ERROR",
Content: "Error occurred while executing build.sh, returning error",
Prompt: false,
})
return 0, tempDir, err, ErrBuildEPKExecutingBuildShError
} }
logger.LogFunc(Log{ // Hopefully, the build was successful. Let's give the user a file count.
Level: "INFO",
Content: "Build finished",
Prompt: false,
})
// Hopefully, the build was successful. Let's give the user a file and size count.
var fileCount int var fileCount int
var sizeCount int64
// We start at -1 because the root directory is not counted // We start at -1 because the root directory is not counted
dirCount := -1 dirCount := -1
err = filepath.Walk(targetRoot, func(path string, info os.FileInfo, err error) error { err = filepath.Walk(targetRoot, func(path string, info os.FileInfo, err error) error {
@ -368,25 +382,18 @@ func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logge
} else { } else {
fileCount++ fileCount++
} }
// Both directories and files need to have their sizes counted
sizeCount += info.Size()
return nil return nil
}) })
if err != nil { if err != nil {
return 0, tempDir, err, ErrBuildEPKCountingFilesError return tempDir, err, ErrBuildEPKCountingFilesError
} }
logger.LogFunc(Log{ logger.LogFunc(Log{
Level: "INFO", Level: "INFO", Content: "Build successful. " + strconv.Itoa(fileCount) + " files and " + strconv.Itoa(dirCount) + " directories created.", Prompt: false,
Content: "Build successful. " + strconv.Itoa(fileCount) + " files and " + strconv.Itoa(dirCount) +
" directories created," + " totalling " + strconv.FormatInt(sizeCount, 10) + " bytes.",
Prompt: false,
}) })
return sizeCount, tempDir, nil, nil
default:
return 0, "", errors.New(buildConfig.Type), ErrBuildEPKBadBuildType
} }
return tempDir, nil, nil
} }
// CreateTar creates a tar archive from a directory // CreateTar creates a tar archive from a directory
@ -397,7 +404,7 @@ func CreateTar(targetDir string, output io.Writer) error {
return err return err
} }
if !info.Mode().IsRegular() && info.Mode()&os.ModeSymlink == 0 { if !info.Mode().IsRegular() {
return nil return nil
} }
@ -408,20 +415,11 @@ func CreateTar(targetDir string, output io.Writer) error {
header.Name = strings.TrimPrefix(strings.Replace(path, targetDir, "", -1), string(filepath.Separator)) header.Name = strings.TrimPrefix(strings.Replace(path, targetDir, "", -1), string(filepath.Separator))
if info.Mode()&os.ModeSymlink != 0 {
linkTarget, err := os.Readlink(path)
if err != nil {
return err
}
header.Linkname = linkTarget
}
err = tarWriter.WriteHeader(header) err = tarWriter.WriteHeader(header)
if err != nil { if err != nil {
return err return err
} }
if info.Mode().IsRegular() {
file, err := os.Open(path) file, err := os.Open(path)
if err != nil { if err != nil {
return err return err
@ -436,7 +434,6 @@ func CreateTar(targetDir string, output io.Writer) error {
if err != nil { if err != nil {
return err return err
} }
}
return nil return nil
}) })
@ -466,13 +463,13 @@ var ConstPackageEPKBigEndian = []byte{0x62}
var ConstPackageEPKLittleEndian = []byte{0x6C} var ConstPackageEPKLittleEndian = []byte{0x6C}
// ConstPackageEPKInitialByteOffset is the initial byte offset for an EPK file until we arrive at the signature. 12 = 3 + 1 + 8: 3 for the magic number, 1 for the endian, and 8 for the tar offset // ConstPackageEPKInitialByteOffset is the initial byte offset for an EPK file until we arrive at the signature. 12 = 3 + 1 + 8: 3 for the magic number, 1 for the endian, and 8 for the tar offset
var ConstPackageEPKInitialByteOffset int64 = 12 var ConstPackageEPKInitialByteOffset = 12
// ConstPackageEPKSignatureLength is the length of the signature // SignatureLength is the length of the signature
var ConstPackageEPKSignatureLength int64 = 64 var SignatureLength = 64
// ConstPackageEPKPublicKeyLength is the length of the public key // PublicKeyLength is the length of the public key
var ConstPackageEPKPublicKeyLength int64 = 32 var PublicKeyLength = 32
// ConstPackageEPKMetadataOffset is the offset of the metadata in the EPK file // ConstPackageEPKMetadataOffset is the offset of the metadata in the EPK file
var ConstPackageEPKMetadataOffset = 108 var ConstPackageEPKMetadataOffset = 108
@ -513,8 +510,8 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
return err, ErrPackageEPKMoveToDistError return err, ErrPackageEPKMoveToDistError
} }
if build.HooksFolder != "" { if build.HooksFolder.Null != true {
hooksDir := filepath.Join(tempDir, build.HooksFolder) hooksDir := filepath.Join(tempDir, build.HooksFolder.Value)
err = os.Rename(hooksDir, distDir+"/hooks") err = os.Rename(hooksDir, distDir+"/hooks")
if err != nil { if err != nil {
return err, ErrPackageEPKMoveToDistError return err, ErrPackageEPKMoveToDistError
@ -539,7 +536,6 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
"noDelete": metaData.SpecialFiles.NoDelete, "noDelete": metaData.SpecialFiles.NoDelete,
"noReplace": metaData.SpecialFiles.NoReplace, "noReplace": metaData.SpecialFiles.NoReplace,
}, },
"size": metaData.DecompressedSize,
} }
// Make the data template into a JSON string // Make the data template into a JSON string
@ -560,7 +556,8 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
} }
// Calculate the tar offset // Calculate the tar offset
var tarOffset int64 = int64(ConstPackageEPKMetadataOffset) + dataTemplateLength var tarOffset int64
tarOffset = int64(ConstPackageEPKMetadataOffset) + dataTemplateLength
logger.LogFunc(Log{ logger.LogFunc(Log{
Level: "INFO", Content: "Calculating binary properties", Prompt: false, Level: "INFO", Content: "Calculating binary properties", Prompt: false,
@ -649,14 +646,14 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
} }
// Create the hash writer // Create the hash writer
xxHash := xxhash.New() sha512Hash := xxhash.New()
_, err = xxHash.Write(dataTemplateBytes) _, err = sha512Hash.Write(dataTemplateBytes)
if err != nil { if err != nil {
return err, nil return err, nil
} }
// Create a multi-writer so we can write to the file and the hash at the same time // Create a multi-writer so we can write to the file and the hash at the same time
multiWriter := io.MultiWriter(file, xxHash) multiWriter := io.MultiWriter(file, sha512Hash)
// Create the ZStandard writer // Create the ZStandard writer
writer, err := zstd.NewWriter(multiWriter, zstd.WithEncoderLevel(zstd.SpeedDefault)) writer, err := zstd.NewWriter(multiWriter, zstd.WithEncoderLevel(zstd.SpeedDefault))
@ -682,7 +679,7 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
}) })
// Sign the hash // Sign the hash
signature := ed25519.Sign(privateKey, xxHash.Sum(nil)) signature := ed25519.Sign(privateKey, sha512Hash.Sum(nil))
publicKey := privateKey.Public().(ed25519.PublicKey) publicKey := privateKey.Public().(ed25519.PublicKey)
// Write the signature and public key to the file // Write the signature and public key to the file
@ -693,13 +690,13 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
} }
// Write the signature // Write the signature
_, err = file.WriteAt(signature, ConstPackageEPKInitialByteOffset) _, err = file.WriteAt(signature, int64(ConstPackageEPKInitialByteOffset))
if err != nil { if err != nil {
return err, ErrPackageEPKCannotWriteFile return err, ErrPackageEPKCannotWriteFile
} }
// Write the public key // Write the public key
_, err = file.WriteAt(publicKey, ConstPackageEPKInitialByteOffset+ConstPackageEPKSignatureLength) _, err = file.WriteAt(publicKey, int64(ConstPackageEPKInitialByteOffset)+int64(SignatureLength))
if err != nil { if err != nil {
return err, ErrPackageEPKCannotWriteFile return err, ErrPackageEPKCannotWriteFile
} }
@ -712,276 +709,3 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
return nil, nil return nil, nil
} }
// ConstGenerateRepositoryRepoDataOffset is the offset of the repository data in the repository.json file: it is 3 (magic) + 64 (the signature) + 32 (the public key) = 99
var ConstGenerateRepositoryRepoDataOffset int64 = 99
// ConstGenerateRepositoryEPKMagicNumber is the magic number for an EPK repository: "eon" in ASCII / UTF-8, for obvious reasons
var ConstGenerateRepositoryEPKMagicNumber = []byte{0x65, 0x6F, 0x6E}
var ErrGenerateRepositoryStatError = errors.New("error stating file or directory")
var ErrGenerateRepositoryNotDirectory = errors.New("not a directory")
var ErrGenerateRepositoryRepositoryNameContainsSlash = errors.New("repository name contains a slash")
var ErrGenerateRepositoryFailedToWalk = errors.New("error walking directory")
var ErrGenerateRepositoryCannotUnmarshalJSON = errors.New("error unmarshalling JSON")
var ErrGenerateRepositoryCannotMarshalJSON = errors.New("error marshalling JSON")
var ErrGenerateRepositoryCannotOpenFile = errors.New("error opening file for writing")
var ErrGenerateRepositoryCannotWriteFile = errors.New("error writing to file")
var ErrGenerateRepositoryCannotCloseFile = errors.New("error closing file")
func GenerateRepository(directory string, privateKey ed25519.PrivateKey, logger *Logger) (error, error) {
// First, we need to see if the directory exists
logger.LogFunc(Log{
Level: "INFO", Content: "Generating repository", Prompt: false,
})
info, err := os.Stat(directory)
if err != nil {
return err, ErrGenerateRepositoryStatError
}
if !info.IsDir() {
return nil, ErrGenerateRepositoryNotDirectory
}
// Create the EPK map
epkMap := make(map[string]interface{})
// See if the repository.json file exists
_, err = os.Stat(directory + "/repository.erf")
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return err, ErrGenerateRepositoryStatError
} else {
if logger.PromptSupported {
// Ask the user for the name of the repository
repoName := logger.LogFunc(Log{
Level: "PROMPT", Content: "Enter the name of the repository", Prompt: true,
})
// Check the repository name does not contain any slashes
if strings.Contains(repoName, "/") {
return nil, ErrGenerateRepositoryRepositoryNameContainsSlash
}
// Ask the user for the description of the repository
repoDesc := logger.LogFunc(Log{
Level: "PROMPT", Content: "Enter a short description of the repository", Prompt: true,
})
// Ask the user for the author of the repository
repoAuthor := logger.LogFunc(Log{
Level: "PROMPT",
Content: "Enter your preferred author name. This must be the same as the author name used in " +
"eternity.json and associated with your keypair, otherwise it will cause issues with EPK" +
" verification and your repository will be rejected by Eon and cannot be trusted.",
Prompt: true,
})
// Now append the metadata to the EPK map
epkMap["name"] = repoName
epkMap["desc"] = repoDesc
epkMap["author"] = repoAuthor
} else {
logger.LogFunc(Log{
Level: "FATAL",
Content: "Please fill in the author, name, and description of the repository in repository.json. " +
"Your author name must be the same as the author name used in eternity.json and associated with " +
"your keypair, otherwise it will cause issues with EPK verification and your repository will be " +
"rejected by Eon and cannot be trusted.",
Prompt: false,
})
}
}
} else {
// Since it does exist, we can extract the name and description from it
file, err := os.ReadFile(directory + "/repository.erf")
if err != nil {
return err, ErrGenerateRepositoryCannotOpenFile
}
// Unmarshal the JSON
var oldRepositoryMap map[string]interface{}
err = json.Unmarshal(file[ConstGenerateRepositoryRepoDataOffset:], &oldRepositoryMap)
if err != nil {
return err, ErrGenerateRepositoryCannotUnmarshalJSON
}
// Copy the author, name, and description to the EPK map
epkMap["name"] = oldRepositoryMap["name"]
epkMap["desc"] = oldRepositoryMap["desc"]
epkMap["author"] = oldRepositoryMap["author"]
}
// Add a list of packages to the EPK map
epkMap["packages"] = make([]map[string]interface{}, 0)
// Now, walk the directory
err = filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
// If error is not nil, return it
if err != nil {
return err
}
// Ignore directories
if info.IsDir() {
return nil
}
// Ok. We need to check if the file actually is an EPK file
file, err := os.Open(path)
if err != nil {
return err
}
// Read the first 3 bytes
magicNumber := make([]byte, 3)
_, err = file.Read(magicNumber)
if err != nil {
return err
}
// Check if the magic number is correct
if !bytes.Equal(magicNumber, ConstPackageEPKMagicNumber) {
// It isn't an EPK file, so we can ignore it
return nil
}
// We need to create a hash of the file
xxHash := xxhash.New()
_, err = io.Copy(xxHash, file)
if err != nil {
return err
}
// Extract the metadata. First, we get the endian-ness
var littleEndian bool
endian := make([]byte, 1)
_, err = file.ReadAt(endian, 3)
if err != nil {
return err
}
if bytes.Equal(endian, ConstPackageEPKLittleEndian) {
littleEndian = true
} else if bytes.Equal(endian, ConstPackageEPKBigEndian) {
littleEndian = false
} else {
return errors.New("invalid endianness")
}
// Now we get the tar offset
var tarOffset int64
tarOffsetBytes := make([]byte, 8)
_, err = file.ReadAt(tarOffsetBytes, 4)
if err != nil {
return err
}
// Now we convert the tar offset to an int64
if littleEndian {
tarOffset = int64(binary.LittleEndian.Uint64(tarOffsetBytes))
} else {
tarOffset = int64(binary.BigEndian.Uint64(tarOffsetBytes))
}
// Now we can read in the metadata
metadataBytes := make([]byte, tarOffset-int64(ConstPackageEPKMetadataOffset))
_, err = file.ReadAt(metadataBytes, int64(ConstPackageEPKMetadataOffset))
if err != nil {
return err
}
// Now we can unmarshal the metadata
var metadata map[string]interface{}
err = json.Unmarshal(metadataBytes, &metadata)
if err != nil {
return err
}
// Now we have the hash, we need to add it to our data template
dataTemplate := make(map[string]interface{})
dataTemplate["hash"] = xxHash.Sum64()
// Now we add some basic metadata
dataTemplate["name"] = metadata["name"]
dataTemplate["author"] = metadata["author"]
dataTemplate["version"] = metadata["version"]
dataTemplate["size"] = metadata["size"]
dataTemplate["arch"] = metadata["arch"]
dataTemplate["desc"] = metadata["desc"]
dataTemplate["deps"] = metadata["deps"]
// We add the path to the EPK file, relative to the directory
relativePath, err := filepath.Rel(directory, path)
if err != nil {
return err
}
dataTemplate["path"] = relativePath
// Append it to a list in the EPK map
epkMap["packages"] = append(epkMap["packages"].([]map[string]interface{}), dataTemplate)
return nil
})
// This error message is a bit vague, but meh.
if err != nil {
return err, ErrGenerateRepositoryFailedToWalk
}
// Great, now we need to marshal the EPK map and write it to a file
epkMapBytes, err := json.Marshal(epkMap)
if err != nil {
return err, ErrGenerateRepositoryCannotMarshalJSON
}
// Write the EPK map to a file
file, err := os.OpenFile(directory+"/repository.erf", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
if err != nil {
return err, ErrGenerateRepositoryCannotOpenFile
}
// Sign the epk map
xxHash := xxhash.New()
_, err = xxHash.Write(epkMapBytes)
if err != nil {
return err, nil
}
signature := ed25519.Sign(privateKey, xxHash.Sum(nil))
publicKey := privateKey.Public().(ed25519.PublicKey)
// Write magic number
_, err = file.Write(ConstGenerateRepositoryEPKMagicNumber)
if err != nil {
return err, ErrGenerateRepositoryCannotWriteFile
}
// Write signature
_, err = file.WriteAt(signature, 3)
if err != nil {
return err, ErrGenerateRepositoryCannotWriteFile
}
// Write public key
_, err = file.WriteAt(publicKey, 67)
if err != nil {
return err, ErrGenerateRepositoryCannotWriteFile
}
// Write the EPK map to the file
_, err = file.WriteAt(epkMapBytes, ConstGenerateRepositoryRepoDataOffset)
if err != nil {
return err, ErrGenerateRepositoryCannotWriteFile
}
// Close the file
err = file.Close()
if err != nil {
return err, ErrGenerateRepositoryCannotCloseFile
}
return nil, nil
}