Compare commits
10 commits
b867612961
...
b64650e114
Author | SHA1 | Date | |
---|---|---|---|
b64650e114 | |||
596fd888cc | |||
6511663dc4 | |||
586135170c | |||
0197437074 | |||
|
a316bcd73c | ||
|
a05c59b044 | ||
|
12bdda79a0 | ||
|
714b62de11 | ||
|
0dbea32590 |
10 changed files with 934 additions and 243 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1 +1,4 @@
|
|||
.idea
|
||||
.vscode
|
||||
/bin
|
||||
*.epk
|
31
README.md
31
README.md
|
@ -3,3 +3,34 @@
|
|||
Eternity is the build tool for eon: It builds packages at speeds RPMBUILD wishes it could.
|
||||
|
||||
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
|
||||
```
|
||||
|
|
13
build.sh
Executable file
13
build.sh
Executable file
|
@ -0,0 +1,13 @@
|
|||
#!/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
BIN
cmd/cmd
Binary file not shown.
239
cmd/main.go
239
cmd/main.go
|
@ -1,12 +1,19 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"eternity/common"
|
||||
"eternity/lib"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.oreonproject.org/oreonproject/eternity/common"
|
||||
"git.oreonproject.org/oreonproject/eternity/lib"
|
||||
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Masterminds/semver"
|
||||
"github.com/sassoftware/go-rpmutils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -34,6 +41,31 @@ 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)
|
||||
if err != nil || vagueError != nil {
|
||||
common.LoadPrivateKeyHandleError(err, vagueError, logger)
|
||||
|
@ -55,11 +87,13 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
tempDir, err, vagueError = lib.BuildEPK("./", inMemory, config.Build, logger)
|
||||
var size int64
|
||||
size, tempDir, err, vagueError = lib.BuildEPK("./", inMemory, config.Build, logger)
|
||||
if err != nil || vagueError != nil {
|
||||
common.BuildEPKHandleError(tempDir, err, vagueError, logger)
|
||||
} else {
|
||||
filePath := filepath.Join("./", config.Metadata.Name+"-"+config.Metadata.Version.String())
|
||||
filePath := filepath.Join(outputDir, config.Metadata.Name+"-"+config.Metadata.Version.String())
|
||||
config.Metadata.DecompressedSize = size
|
||||
err, vagueError := lib.PackageEPK(config.Metadata, config.Build, tempDir, filePath+".epk", privateKey, logger)
|
||||
if err != nil || vagueError != nil {
|
||||
common.PackageEPKHandleError(err, vagueError, logger)
|
||||
|
@ -72,6 +106,201 @@ 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":
|
||||
if len(os.Args) > 2 {
|
||||
switch os.Args[2] {
|
||||
|
@ -79,7 +308,7 @@ func main() {
|
|||
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.")
|
||||
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":
|
||||
fmt.Println("Usage: eternity repo <build/generate> <directory>")
|
||||
fmt.Println(" build Build a repository from a directory containing EPK project directories.")
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.oreonproject.org/oreonproject/eternity/lib"
|
||||
|
||||
"crypto/ed25519"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"eternity/lib"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
@ -35,18 +38,20 @@ var DefaultLogger = lib.Logger{
|
|||
fmt.Println(severityPretty, log.Content)
|
||||
if log.Prompt {
|
||||
fmt.Print(": ")
|
||||
var userInput string
|
||||
_, err := fmt.Scanln(&userInput)
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
userInput, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
fmt.Println(color.RedString("[FATAL]"), "Failed to read user input:", err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
return userInput
|
||||
return userInput[:len(userInput)-1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
},
|
||||
PromptSupported: true,
|
||||
StdoutSupported: true,
|
||||
Stdout: os.Stdout,
|
||||
}
|
||||
|
||||
// EternityJsonHandleError handles errors related to the parsing of eternity.json
|
||||
|
@ -154,6 +159,11 @@ func BuildEPKHandleError(tempDir string, err error, vagueError error, logger *li
|
|||
Level: "FATAL",
|
||||
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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -382,3 +392,50 @@ 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,4 +8,3 @@
|
|||
</magic>
|
||||
</mime-type>
|
||||
</mime-info>
|
||||
|
||||
|
|
11
go.mod
11
go.mod
|
@ -1,4 +1,4 @@
|
|||
module eternity
|
||||
module git.oreonproject.org/oreonproject/eternity
|
||||
|
||||
go 1.23
|
||||
|
||||
|
@ -7,10 +7,17 @@ require (
|
|||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/fatih/color v1.17.0
|
||||
github.com/klauspost/compress v1.17.9
|
||||
github.com/sassoftware/go-rpmutils v0.4.0
|
||||
)
|
||||
|
||||
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-isatty v0.0.20 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // 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
76
go.sum
|
@ -1,7 +1,23 @@
|
|||
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/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/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/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
|
@ -11,7 +27,67 @@ 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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
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.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.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.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=
|
||||
|
|
722
lib/main.go
722
lib/main.go
|
@ -1,19 +1,20 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"archive/tar"
|
||||
"crypto/ed25519"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Masterminds/semver"
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
|
@ -21,43 +22,40 @@ import (
|
|||
|
||||
// SpecialFiles is a struct that contains the special files that are not to be deleted or replaced
|
||||
type SpecialFiles struct {
|
||||
NoDelete []string
|
||||
NoReplace []string
|
||||
NoDelete []string `json:"noDelete"`
|
||||
NoReplace []string `json:"noReplace"`
|
||||
}
|
||||
|
||||
// Metadata is a struct that contains the metadata of the package
|
||||
type Metadata struct {
|
||||
Name string
|
||||
Description string
|
||||
LongDescription string
|
||||
Version semver.Version
|
||||
Author string
|
||||
License string
|
||||
Architecture string
|
||||
Dependencies []string
|
||||
SpecialFiles SpecialFiles
|
||||
Name string `json:"name"`
|
||||
Description string `json:"desc"`
|
||||
LongDescription string `json:"longDesc"`
|
||||
Version *semver.Version
|
||||
VersionString string `json:"version"`
|
||||
Author string `json:"author"`
|
||||
License string `json:"license"`
|
||||
Architecture string `json:"arch"`
|
||||
// The decompressed size may be larger than the int64 allocated for a compressed file
|
||||
DecompressedSize int64
|
||||
Dependencies []string `json:"deps"`
|
||||
SpecialFiles SpecialFiles `json:"specialFiles"`
|
||||
}
|
||||
|
||||
// Build is a struct that contains the build configuration of the package
|
||||
type Build struct {
|
||||
Type string
|
||||
Dependencies []string
|
||||
Steps []string
|
||||
TargetRoot string
|
||||
HooksFolder PotentiallyNullString
|
||||
FilesFolder PotentiallyNullString
|
||||
}
|
||||
|
||||
// PotentiallyNullString is a struct that contains a string that may be null
|
||||
type PotentiallyNullString struct {
|
||||
Value string
|
||||
Null bool
|
||||
Type string `json:"type"`
|
||||
Dependencies []string `json:"deps"`
|
||||
Steps []string `json:"steps"`
|
||||
TargetRoot string `json:"root"`
|
||||
HooksFolder string `json:"hooks"`
|
||||
FilesFolder string `json:"files"`
|
||||
}
|
||||
|
||||
// Config is a struct that contains the configuration of the package
|
||||
type Config struct {
|
||||
Metadata Metadata
|
||||
Build Build
|
||||
Metadata Metadata `json:"metadata"`
|
||||
Build Build `json:"build"`
|
||||
}
|
||||
|
||||
// Log is a struct that contains the log information
|
||||
|
@ -71,6 +69,8 @@ type Log struct {
|
|||
type Logger struct {
|
||||
LogFunc func(Log) string
|
||||
PromptSupported bool
|
||||
StdoutSupported bool
|
||||
Stdout io.Writer
|
||||
}
|
||||
|
||||
var ErrEternityJsonOpenError = errors.New("error opening eternity.json")
|
||||
|
@ -78,22 +78,6 @@ var ErrEternityJsonReadError = errors.New("error reading eternity.json")
|
|||
var ErrEternityJsonParseError = errors.New("error parsing 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
|
||||
func ParseConfig(path string, logger *Logger) (Config, error, error) {
|
||||
// Open eternity.json
|
||||
|
@ -107,143 +91,22 @@ func ParseConfig(path string, logger *Logger) (Config, error, error) {
|
|||
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
|
||||
var config map[string]interface{}
|
||||
err = json.Unmarshal(fileBytes.Bytes(), &config)
|
||||
var config Config
|
||||
decoder := json.NewDecoder(file)
|
||||
err = decoder.Decode(&config)
|
||||
if err != nil {
|
||||
return Config{}, err, ErrEternityJsonParseError
|
||||
}
|
||||
|
||||
// Map SpecialFiles
|
||||
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")
|
||||
// Map the JSON version to a semver version
|
||||
config.Metadata.Version, err = semver.NewVersion(config.Metadata.VersionString)
|
||||
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 {
|
||||
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 parsedConfig, nil, nil
|
||||
return config, nil, nil
|
||||
}
|
||||
|
||||
var ErrBuildEPKTemporaryDirectoryError = errors.New("error creating temporary directory")
|
||||
|
@ -256,48 +119,66 @@ var ErrBuildEPKWritingBuildShError = errors.New("error writing to build.sh")
|
|||
var ErrBuildEPKTargetRootError = errors.New("error creating target root")
|
||||
var ErrBuildEPKExecutingBuildShError = errors.New("error executing build.sh")
|
||||
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
|
||||
func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logger) (string, error, error) {
|
||||
func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logger) (int64, string, error, error) {
|
||||
var tempDir string
|
||||
|
||||
switch buildConfig.Type {
|
||||
case "chroot":
|
||||
return "", nil, ErrBuildEPKChrootError
|
||||
return 0, "", nil, ErrBuildEPKChrootError
|
||||
case "unrestricted":
|
||||
return "", nil, ErrBuildEPKUnrestrictedError
|
||||
return 0, "", nil, ErrBuildEPKUnrestrictedError
|
||||
case "host":
|
||||
// Set up the temp dir
|
||||
var err error
|
||||
if inMemory {
|
||||
// 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.
|
||||
logger.LogFunc(Log{
|
||||
Level: "INFO",
|
||||
Content: "Creating temp directory",
|
||||
Prompt: false,
|
||||
})
|
||||
tempDir, err = os.MkdirTemp("/tmp", "eternity-build-")
|
||||
} else {
|
||||
// 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.
|
||||
logger.LogFunc(Log{
|
||||
Level: "INFO",
|
||||
Content: "Creating temp directory on disk",
|
||||
Prompt: false,
|
||||
})
|
||||
tempDir, err = os.MkdirTemp(projectDir, "eternity-build-")
|
||||
}
|
||||
if err != nil {
|
||||
return tempDir, err, ErrBuildEPKTemporaryDirectoryError
|
||||
logger.LogFunc(Log{
|
||||
Level: "INFO",
|
||||
Content: "Failed to create temporary directory, returning the error",
|
||||
Prompt: false,
|
||||
})
|
||||
return 0, tempDir, err, ErrBuildEPKTemporaryDirectoryError
|
||||
}
|
||||
|
||||
// Copy the hooks folder
|
||||
if buildConfig.HooksFolder.Null != true {
|
||||
hooksDir := filepath.Join(projectDir, buildConfig.HooksFolder.Value)
|
||||
targetHooksDir := filepath.Join(tempDir, buildConfig.HooksFolder.Value)
|
||||
if buildConfig.HooksFolder != "" {
|
||||
hooksDir := filepath.Join(projectDir, buildConfig.HooksFolder)
|
||||
targetHooksDir := filepath.Join(tempDir, buildConfig.HooksFolder)
|
||||
logger.LogFunc(Log{
|
||||
Level: "INFO", Content: "Copying hooks from " + hooksDir + " to " + targetHooksDir, Prompt: false,
|
||||
})
|
||||
|
||||
err = os.MkdirAll(targetHooksDir, 0755)
|
||||
if err != nil {
|
||||
return tempDir, err, ErrBuildEPKCreateHooksError
|
||||
return 0, tempDir, err, ErrBuildEPKCreateHooksError
|
||||
}
|
||||
|
||||
err = os.CopyFS(targetHooksDir, os.DirFS(hooksDir))
|
||||
if err != nil {
|
||||
return tempDir, err, ErrBuildEPKCopyHooksError
|
||||
return 0, tempDir, err, ErrBuildEPKCopyHooksError
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -314,19 +195,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)
|
||||
if err != nil {
|
||||
return tempDir, err, ErrBuildEPKBuildShError
|
||||
return 0, tempDir, err, ErrBuildEPKBuildShError
|
||||
}
|
||||
|
||||
_, err = file.WriteString(shellScript)
|
||||
if err != nil {
|
||||
return tempDir, err, ErrBuildEPKWritingBuildShError
|
||||
return 0, tempDir, err, ErrBuildEPKWritingBuildShError
|
||||
}
|
||||
|
||||
// Set up the target root
|
||||
targetRoot := filepath.Join(tempDir, buildConfig.TargetRoot)
|
||||
err = os.MkdirAll(targetRoot, 0755)
|
||||
if err != nil {
|
||||
return tempDir, err, ErrBuildEPKTargetRootError
|
||||
return 0, tempDir, err, ErrBuildEPKTargetRootError
|
||||
}
|
||||
|
||||
// Execute the shell script in BWrap
|
||||
|
@ -338,6 +219,62 @@ 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
|
||||
// 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)
|
||||
// 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{
|
||||
"--unshare-net",
|
||||
"--bind", "/bin", "/bin",
|
||||
|
@ -354,26 +291,75 @@ func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logge
|
|||
"--tmpfs", "/run",
|
||||
"--tmpfs", "/tmp",
|
||||
"--proc", "/proc",
|
||||
"/usr/bin/fakeroot-tcp", "--",
|
||||
"/usr/bin/fakeroot-tcp", "--", // fakeroot-tcp still installs as "fakeroot" for some reason TODO: handle wrong fakeroot installation
|
||||
"/bin/sh", "/eternity/build.sh",
|
||||
}
|
||||
|
||||
if buildConfig.FilesFolder.Null != true {
|
||||
if buildConfig.FilesFolder != "" {
|
||||
logger.LogFunc(Log{
|
||||
Level: "INFO",
|
||||
Content: "Binding files folder to container",
|
||||
Prompt: false,
|
||||
})
|
||||
arguments = arguments[:len(arguments)-4]
|
||||
arguments = append(
|
||||
arguments, "--bind", filepath.Join(projectDir, buildConfig.FilesFolder.Value), filepath.Join("/", buildConfig.FilesFolder.Value),
|
||||
arguments, "--bind", filepath.Join(projectDir, buildConfig.FilesFolder), filepath.Join("/", buildConfig.FilesFolder),
|
||||
"/usr/bin/fakeroot-tcp", "--",
|
||||
"/bin/sh", "/eternity/build.sh",
|
||||
)
|
||||
}
|
||||
|
||||
err = exec.Command("bwrap", arguments...).Run()
|
||||
logger.LogFunc(Log{
|
||||
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 {
|
||||
return tempDir, err, ErrBuildEPKExecutingBuildShError
|
||||
logger.LogFunc(Log{
|
||||
Level: "ERROR",
|
||||
Content: "Error occurred while executing build.sh, returning error",
|
||||
Prompt: false,
|
||||
})
|
||||
return 0, tempDir, err, ErrBuildEPKExecutingBuildShError
|
||||
}
|
||||
|
||||
// Hopefully, the build was successful. Let's give the user a file count.
|
||||
logger.LogFunc(Log{
|
||||
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 sizeCount int64
|
||||
// We start at -1 because the root directory is not counted
|
||||
dirCount := -1
|
||||
err = filepath.Walk(targetRoot, func(path string, info os.FileInfo, err error) error {
|
||||
|
@ -382,18 +368,25 @@ func BuildEPK(projectDir string, inMemory bool, buildConfig Build, logger *Logge
|
|||
} else {
|
||||
fileCount++
|
||||
}
|
||||
// Both directories and files need to have their sizes counted
|
||||
sizeCount += info.Size()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return tempDir, err, ErrBuildEPKCountingFilesError
|
||||
return 0, tempDir, err, ErrBuildEPKCountingFilesError
|
||||
}
|
||||
|
||||
logger.LogFunc(Log{
|
||||
Level: "INFO", Content: "Build successful. " + strconv.Itoa(fileCount) + " files and " + strconv.Itoa(dirCount) + " directories created.", Prompt: false,
|
||||
Level: "INFO",
|
||||
Content: "Build successful. " + strconv.Itoa(fileCount) + " files and " + strconv.Itoa(dirCount) +
|
||||
" directories created," + " totalling " + strconv.FormatInt(sizeCount, 10) + " bytes.",
|
||||
Prompt: false,
|
||||
})
|
||||
}
|
||||
|
||||
return tempDir, nil, nil
|
||||
return sizeCount, tempDir, nil, nil
|
||||
default:
|
||||
return 0, "", errors.New(buildConfig.Type), ErrBuildEPKBadBuildType
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTar creates a tar archive from a directory
|
||||
|
@ -404,7 +397,7 @@ func CreateTar(targetDir string, output io.Writer) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if !info.Mode().IsRegular() {
|
||||
if !info.Mode().IsRegular() && info.Mode()&os.ModeSymlink == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -415,24 +408,34 @@ func CreateTar(targetDir string, output io.Writer) error {
|
|||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Mode().IsRegular() {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(tarWriter, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(tarWriter, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -463,13 +466,13 @@ var ConstPackageEPKBigEndian = []byte{0x62}
|
|||
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
|
||||
var ConstPackageEPKInitialByteOffset = 12
|
||||
var ConstPackageEPKInitialByteOffset int64 = 12
|
||||
|
||||
// SignatureLength is the length of the signature
|
||||
var SignatureLength = 64
|
||||
// ConstPackageEPKSignatureLength is the length of the signature
|
||||
var ConstPackageEPKSignatureLength int64 = 64
|
||||
|
||||
// PublicKeyLength is the length of the public key
|
||||
var PublicKeyLength = 32
|
||||
// ConstPackageEPKPublicKeyLength is the length of the public key
|
||||
var ConstPackageEPKPublicKeyLength int64 = 32
|
||||
|
||||
// ConstPackageEPKMetadataOffset is the offset of the metadata in the EPK file
|
||||
var ConstPackageEPKMetadataOffset = 108
|
||||
|
@ -510,8 +513,8 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
|
|||
return err, ErrPackageEPKMoveToDistError
|
||||
}
|
||||
|
||||
if build.HooksFolder.Null != true {
|
||||
hooksDir := filepath.Join(tempDir, build.HooksFolder.Value)
|
||||
if build.HooksFolder != "" {
|
||||
hooksDir := filepath.Join(tempDir, build.HooksFolder)
|
||||
err = os.Rename(hooksDir, distDir+"/hooks")
|
||||
if err != nil {
|
||||
return err, ErrPackageEPKMoveToDistError
|
||||
|
@ -536,6 +539,7 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
|
|||
"noDelete": metaData.SpecialFiles.NoDelete,
|
||||
"noReplace": metaData.SpecialFiles.NoReplace,
|
||||
},
|
||||
"size": metaData.DecompressedSize,
|
||||
}
|
||||
|
||||
// Make the data template into a JSON string
|
||||
|
@ -556,8 +560,7 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
|
|||
}
|
||||
|
||||
// Calculate the tar offset
|
||||
var tarOffset int64
|
||||
tarOffset = int64(ConstPackageEPKMetadataOffset) + dataTemplateLength
|
||||
var tarOffset int64 = int64(ConstPackageEPKMetadataOffset) + dataTemplateLength
|
||||
|
||||
logger.LogFunc(Log{
|
||||
Level: "INFO", Content: "Calculating binary properties", Prompt: false,
|
||||
|
@ -646,14 +649,14 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
|
|||
}
|
||||
|
||||
// Create the hash writer
|
||||
sha512Hash := xxhash.New()
|
||||
_, err = sha512Hash.Write(dataTemplateBytes)
|
||||
xxHash := xxhash.New()
|
||||
_, err = xxHash.Write(dataTemplateBytes)
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
|
||||
// Create a multi-writer so we can write to the file and the hash at the same time
|
||||
multiWriter := io.MultiWriter(file, sha512Hash)
|
||||
multiWriter := io.MultiWriter(file, xxHash)
|
||||
|
||||
// Create the ZStandard writer
|
||||
writer, err := zstd.NewWriter(multiWriter, zstd.WithEncoderLevel(zstd.SpeedDefault))
|
||||
|
@ -679,7 +682,7 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
|
|||
})
|
||||
|
||||
// Sign the hash
|
||||
signature := ed25519.Sign(privateKey, sha512Hash.Sum(nil))
|
||||
signature := ed25519.Sign(privateKey, xxHash.Sum(nil))
|
||||
publicKey := privateKey.Public().(ed25519.PublicKey)
|
||||
|
||||
// Write the signature and public key to the file
|
||||
|
@ -690,13 +693,13 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
|
|||
}
|
||||
|
||||
// Write the signature
|
||||
_, err = file.WriteAt(signature, int64(ConstPackageEPKInitialByteOffset))
|
||||
_, err = file.WriteAt(signature, ConstPackageEPKInitialByteOffset)
|
||||
if err != nil {
|
||||
return err, ErrPackageEPKCannotWriteFile
|
||||
}
|
||||
|
||||
// Write the public key
|
||||
_, err = file.WriteAt(publicKey, int64(ConstPackageEPKInitialByteOffset)+int64(SignatureLength))
|
||||
_, err = file.WriteAt(publicKey, ConstPackageEPKInitialByteOffset+ConstPackageEPKSignatureLength)
|
||||
if err != nil {
|
||||
return err, ErrPackageEPKCannotWriteFile
|
||||
}
|
||||
|
@ -709,3 +712,276 @@ func PackageEPK(metaData Metadata, build Build, tempDir string, output string, p
|
|||
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue