This commit is contained in:
itexpert228 2026-03-09 22:40:05 +03:00
parent e7c9f87cac
commit e351ee62d1
No known key found for this signature in database
19 changed files with 1915 additions and 2 deletions

48
.gitignore vendored Normal file
View file

@ -0,0 +1,48 @@
# Binary executables
zsvo
pkg-manager
# Build artifacts
*.exe
*.dll
*.so
*.dylib
# Temporary files
*.tmp
*.temp
*.log
# IDE files
.vscode/
.idea/
*.swp
*.swo
# OS files
.DS_Store
Thumbs.db
# Cache directories
.cache/
tmp/
temp/
# Test coverage
coverage.txt
*.coverprofile
# Dependency directories
vendor/
node_modules/
# Environment files
.env
.env.local
.env.development
.env.production
# Backup files
*.bak
*.backup
*.orig

160
README.md
View file

@ -1,3 +1,159 @@
# zsvo
# Package Manager
ZSVO - ZOVOS Software Versioning Operator, пакетный менеджер для дистрибутива ZOV OS
A minimal source-based package manager for custom Linux distributions, inspired by Arch Linux and Gentoo.
## Project Structure
```
zsvo/
├── go.mod # Go module definition
├── go.sum # Go dependencies
├── main.go # CLI entry point
├── cmd/ # CLI commands
│ ├── build.go # pkg build command
│ ├── install.go # pkg install command
│ ├── remove.go # pkg remove command
│ ├── list.go # pkg list command
│ └── info.go # pkg info command
├── pkg/ # Core packages
│ ├── recipe/ # Recipe parsing and validation
│ │ └── recipe.go
│ ├── fetcher/ # Source downloading and extraction
│ │ └── fetcher.go
│ ├── builder/ # Package building
│ │ └── builder.go
│ ├── packager/ # Package creation
│ │ └── packager.go
│ └── installer/ # Package installation/removal
│ └── installer.go
└── recipes/ # Example recipes
└── zlib.toml
```
## Modules
### Recipe Module (`pkg/recipe/`)
- Parses TOML recipe files
- Validates recipe structure
- Provides package naming utilities
- Defines `Recipe`, `Source`, `Build`, and `PkgInfo` structs
### Fetcher Module (`pkg/fetcher/`)
- Downloads sources with checksum verification
- Extracts various archive formats
- Handles patch application
- Implements caching for downloaded sources
- Supports concurrent downloads
### Builder Module (`pkg/builder/`)
- Manages build process from recipe
- Sets up build environment
- Executes build commands
- Handles source directory management
- Provides build information and file listing
### Packager Module (`pkg/packager/`)
- Creates compressed package archives
- Generates package metadata (.pkginfo)
- Extracts packages for installation
- Verifies package integrity
- Lists package contents
### Installer Module (`pkg/installer/`)
- Installs packages to root filesystem
- Manages package database
- Handles dependency checking
- Removes packages and cleans up
- Provides package information queries
## Usage
### Building a Package
```bash
# Build from recipe
pkg build recipes/zlib.toml
# Build with custom work directory
pkg build -w /tmp/build zlib.toml
```
### Installing a Package
```bash
# Install package
pkg install /path/to/package.pkg.tar.zst
# Install to custom root
pkg install -r /mnt/root package.pkg.tar.zst
```
### Managing Packages
```bash
# List installed packages
pkg list
# Show package information
pkg info zlib
# Remove package
pkg remove zlib
```
## Recipe Format
Recipes are written in TOML format:
```toml
name = "package-name"
version = "1.0.0"
description = "Package description"
[source]
url = "https://example.com/package-1.0.0.tar.gz"
sha256 = "sha256-hash-of-source"
patches = ["patch1.diff", "patch2.diff"]
[build]
commands = [
"./configure --prefix=/usr",
"make -j$(nproc)",
"make DESTDIR={{pkgdir}} install"
]
env = [
"CFLAGS=-O2"
]
[dependencies]
# List of package dependencies
```
## Package Format
Packages are created as `name-version.pkg.tar.zst` archives containing:
- Installed files
- `.pkginfo` metadata file
## Package Database
Installed packages are tracked in `/var/lib/pkgdb/<pkgname>/` with:
- `.pkginfo` file containing package metadata
- File lists and installation information
## Dependencies
- `github.com/BurntSushi/toml` - TOML parsing
- `github.com/mholt/archiver/v4` - Archive handling
- `github.com/spf13/cobra` - CLI framework
- `github.com/spf13/viper` - Configuration management
- `golang.org/x/sync` - Concurrent operations
## Philosophy
This package manager follows the simplicity philosophy of Arch Linux tools:
- Minimal dependencies
- Clear separation of concerns
- Production-ready code
- Source-based package management
- Simple, understandable implementation

46
cmd/build.go Normal file
View file

@ -0,0 +1,46 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"zsvo/pkg/builder"
"zsvo/pkg/recipe"
)
var BuildCmd = &cobra.Command{
Use: "build <recipe>",
Short: "Build a package from recipe",
Long: `Build a package from a recipe file`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
recipePath := args[0]
// Parse recipe
recipe, err := recipe.ParseRecipe(recipePath)
if err != nil {
return fmt.Errorf("failed to parse recipe: %w", err)
}
// Create builder
workDir, _ := cmd.Flags().GetString("work-dir")
if workDir == "" {
workDir = "/tmp/pkg-work"
}
b := builder.NewBuilder(workDir)
// Build package
fmt.Printf("Building package %s...\n", recipe.GetPackageName())
if err := b.Build(recipe); err != nil {
return fmt.Errorf("failed to build package: %w", err)
}
fmt.Printf("Package %s built successfully\n", recipe.GetPackageName())
return nil
},
}
func init() {
BuildCmd.Flags().StringP("work-dir", "w", "", "Working directory for build")
}

51
cmd/info.go Normal file
View file

@ -0,0 +1,51 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"zsvo/pkg/installer"
)
var InfoCmd = &cobra.Command{
Use: "info <package>",
Short: "Show package information",
Long: `Show information about an installed package`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
packageName := args[0]
// Create installer
rootDir, _ := cmd.Flags().GetString("root")
if rootDir == "" {
rootDir = "/"
}
i := installer.NewInstaller(rootDir)
// Get package info
pkgInfo, err := i.GetPackageInfo(packageName)
if err != nil {
return fmt.Errorf("failed to get package info: %w", err)
}
fmt.Printf("Package: %s\n", pkgInfo.Name)
fmt.Printf("Version: %s\n", pkgInfo.Version)
if pkgInfo.Description != "" {
fmt.Printf("Description: %s\n", pkgInfo.Description)
}
if len(pkgInfo.Dependencies) > 0 {
fmt.Printf("Dependencies: %v\n", pkgInfo.Dependencies)
}
fmt.Printf("Install date: %s\n", pkgInfo.InstallDate)
fmt.Printf("Files (%d):\n", len(pkgInfo.Files))
for _, file := range pkgInfo.Files {
fmt.Printf(" %s\n", file)
}
return nil
},
}
func init() {
InfoCmd.Flags().StringP("root", "r", "/", "Root directory for installation")
}

39
cmd/install.go Normal file
View file

@ -0,0 +1,39 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"zsvo/pkg/installer"
)
var InstallCmd = &cobra.Command{
Use: "install <package>",
Short: "Install a package",
Long: `Install a package from a package file`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
packagePath := args[0]
// Create installer
rootDir, _ := cmd.Flags().GetString("root")
if rootDir == "" {
rootDir = "/"
}
i := installer.NewInstaller(rootDir)
// Install package
fmt.Printf("Installing package from %s...\n", packagePath)
if err := i.Install(packagePath); err != nil {
return fmt.Errorf("failed to install package: %w", err)
}
fmt.Printf("Package installed successfully\n")
return nil
},
}
func init() {
InstallCmd.Flags().StringP("root", "r", "/", "Root directory for installation")
}

44
cmd/list.go Normal file
View file

@ -0,0 +1,44 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"zsvo/pkg/installer"
)
var ListCmd = &cobra.Command{
Use: "list",
Short: "List installed packages",
Long: `List all installed packages`,
RunE: func(cmd *cobra.Command, args []string) error {
// Create installer
rootDir, _ := cmd.Flags().GetString("root")
if rootDir == "" {
rootDir = "/"
}
i := installer.NewInstaller(rootDir)
// List installed packages
packages, err := i.ListInstalled()
if err != nil {
return fmt.Errorf("failed to list packages: %w", err)
}
if len(packages) == 0 {
fmt.Println("No packages installed")
return nil
}
fmt.Println("Installed packages:")
for _, pkg := range packages {
fmt.Printf(" %s\n", pkg)
}
return nil
},
}
func init() {
ListCmd.Flags().StringP("root", "r", "/", "Root directory for installation")
}

39
cmd/remove.go Normal file
View file

@ -0,0 +1,39 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"zsvo/pkg/installer"
)
var RemoveCmd = &cobra.Command{
Use: "remove <package>",
Short: "Remove a package",
Long: `Remove an installed package`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
packageName := args[0]
// Create installer
rootDir, _ := cmd.Flags().GetString("root")
if rootDir == "" {
rootDir = "/"
}
i := installer.NewInstaller(rootDir)
// Remove package
fmt.Printf("Removing package %s...\n", packageName)
if err := i.Remove(packageName); err != nil {
return fmt.Errorf("failed to remove package: %w", err)
}
fmt.Printf("Package %s removed successfully\n", packageName)
return nil
},
}
func init() {
RemoveCmd.Flags().StringP("root", "r", "/", "Root directory for installation")
}

24
go.mod Normal file
View file

@ -0,0 +1,24 @@
module zsvo
go 1.23.0
require (
github.com/BurntSushi/toml v1.3.2
github.com/mholt/archiver/v3 v3.5.1
github.com/spf13/cobra v1.8.1
golang.org/x/sync v0.12.0
)
require (
github.com/andybalholm/brotli v1.0.1 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/golang/snappy v0.0.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.17.2 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/nwaples/rardecode v1.1.0 // indirect
github.com/pierrec/lz4/v4 v4.1.2 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/ulikunitz/xz v0.5.9 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
)

41
go.sum Normal file
View file

@ -0,0 +1,41 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM=
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
github.com/ulikunitz/xz v0.5.9/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=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

40
main.go Normal file
View file

@ -0,0 +1,40 @@
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
"zsvo/cmd"
)
var rootCmd = &cobra.Command{
Use: "pkg",
Short: "A simple source-based package manager",
Long: `A minimal package manager for custom Linux distributions based on LFS
Available commands:
build Build a package from recipe
install Install a package from package file
remove Remove an installed package
list List installed packages
info Show package information
help Show help for a command
`,
}
func init() {
// Register all commands
rootCmd.AddCommand(cmd.BuildCmd)
rootCmd.AddCommand(cmd.InstallCmd)
rootCmd.AddCommand(cmd.RemoveCmd)
rootCmd.AddCommand(cmd.ListCmd)
rootCmd.AddCommand(cmd.InfoCmd)
}
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

BIN
pkg-manager Executable file

Binary file not shown.

317
pkg/builder/builder.go Normal file
View file

@ -0,0 +1,317 @@
package builder
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"zsvo/pkg/recipe"
"zsvo/pkg/fetcher"
)
// Builder handles building packages from recipes
type Builder struct {
workDir string
}
// NewBuilder creates a new builder
func NewBuilder(workDir string) *Builder {
return &Builder{
workDir: workDir,
}
}
// Build builds a package from a recipe
func (b *Builder) Build(recipe *recipe.Recipe) error {
// Create working directories
sourceDir := recipe.GetSourceDir(b.workDir)
stagingDir := recipe.GetStagingDir(b.workDir)
packageDir := recipe.GetPackageDir(b.workDir)
if err := b.prepareDirectories(sourceDir, stagingDir, packageDir); err != nil {
return fmt.Errorf("failed to prepare directories: %w", err)
}
// Download and extract source
if err := b.downloadAndExtract(recipe); err != nil {
return fmt.Errorf("failed to download and extract source: %w", err)
}
// Apply patches
if err := b.applyPatches(recipe, sourceDir); err != nil {
return fmt.Errorf("failed to apply patches: %w", err)
}
// Build package
if err := b.executeBuild(recipe, sourceDir, stagingDir); err != nil {
return fmt.Errorf("failed to build package: %w", err)
}
// Package files
if err := b.packageFiles(recipe, sourceDir, stagingDir, packageDir); err != nil {
return fmt.Errorf("failed to package files: %w", err)
}
return nil
}
// prepareDirectories creates necessary directories
func (b *Builder) prepareDirectories(sourceDir, stagingDir, packageDir string) error {
dirs := []string{
sourceDir,
stagingDir,
packageDir,
}
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", dir, err)
}
}
return nil
}
// downloadAndExtract downloads and extracts source
func (b *Builder) downloadAndExtract(recipe *recipe.Recipe) error {
f := fetcher.NewFetcher(filepath.Join(b.workDir, "cache"))
return f.DownloadAndExtract(
recipe.Source.URL,
recipe.Source.Sha256,
recipe.GetSourceDir(b.workDir),
)
}
// applyPatches applies patches to source
func (b *Builder) applyPatches(recipe *recipe.Recipe, sourceDir string) error {
if len(recipe.Source.Patches) == 0 {
return nil
}
f := fetcher.NewFetcher(filepath.Join(b.workDir, "cache"))
return f.ApplyPatches(sourceDir, recipe.Source.Patches)
}
// executeBuild executes build commands
func (b *Builder) executeBuild(recipe *recipe.Recipe, sourceDir, stagingDir string) error {
// Find the source directory (usually the first subdirectory)
srcPath, err := b.findSourceDirectory(sourceDir)
if err != nil {
return fmt.Errorf("failed to find source directory: %w", err)
}
// Set up environment
env := b.buildEnvironment(recipe, stagingDir)
// Execute build commands
for i, cmd := range recipe.Build.Commands {
// Substitute variables in command
cmd = b.substituteVariables(cmd, recipe, sourceDir, stagingDir)
if err := b.executeCommand(srcPath, cmd, env); err != nil {
return fmt.Errorf("build command %d failed: %w", i+1, err)
}
}
return nil
}
// findSourceDirectory finds the actual source directory
func (b *Builder) findSourceDirectory(sourceDir string) (string, error) {
// Validate input path
if sourceDir == "" {
return "", fmt.Errorf("source directory cannot be empty")
}
// Clean and normalize path
sourceDir = filepath.Clean(sourceDir)
entries, err := os.ReadDir(sourceDir)
if err != nil {
return "", fmt.Errorf("failed to read source directory %s: %w", sourceDir, err)
}
for _, entry := range entries {
if entry.IsDir() {
return filepath.Join(sourceDir, entry.Name()), nil
}
}
return sourceDir, nil
}
// buildEnvironment builds the environment for build commands
func (b *Builder) buildEnvironment(recipe *recipe.Recipe, stagingDir string) []string {
env := os.Environ()
// Add standard build environment variables
env = append(env, fmt.Sprintf("DESTDIR=%s", stagingDir))
env = append(env, fmt.Sprintf("PREFIX=/usr"))
env = append(env, fmt.Sprintf("PKGDIR=%s", stagingDir))
// Add recipe-specific environment variables
for _, envVar := range recipe.Build.Env {
env = append(env, envVar)
}
// Add parallel build variable
env = append(env, fmt.Sprintf("MAKEFLAGS=-j%d", runtime.NumCPU()))
return env
}
// executeCommand executes a single command
func (b *Builder) executeCommand(workDir, command string, env []string) error {
// Parse command
args := strings.Fields(command)
if len(args) == 0 {
return nil
}
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = workDir
cmd.Env = env
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// executeCommandWithOutput executes a command and returns output
func (b *Builder) executeCommandWithOutput(workDir, command string, env []string) (string, error) {
args := strings.Fields(command)
if len(args) == 0 {
return "", nil
}
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = workDir
cmd.Env = env
output, err := cmd.CombinedOutput()
return string(output), err
}
// Clean removes build artifacts
func (b *Builder) Clean(recipe *recipe.Recipe) error {
dirs := []string{
recipe.GetSourceDir(b.workDir),
recipe.GetStagingDir(b.workDir),
}
for _, dir := range dirs {
if err := os.RemoveAll(dir); err != nil {
return fmt.Errorf("failed to clean directory %s: %w", dir, err)
}
}
return nil
}
// GetBuildInfo returns information about the build
func (b *Builder) GetBuildInfo(recipe *recipe.Recipe) (*BuildInfo, error) {
sourceDir := recipe.GetSourceDir(b.workDir)
stagingDir := recipe.GetStagingDir(b.workDir)
info := &BuildInfo{
Recipe: recipe,
SourceDir: sourceDir,
StagingDir: stagingDir,
SourceExists: false,
StagingExists: false,
}
// Check if source directory exists
if _, err := os.Stat(sourceDir); err == nil {
info.SourceExists = true
}
// Check if staging directory exists
if _, err := os.Stat(stagingDir); err == nil {
info.StagingExists = true
}
return info, nil
}
// BuildInfo contains information about a build
type BuildInfo struct {
Recipe *recipe.Recipe
SourceDir string
StagingDir string
SourceExists bool
StagingExists bool
}
// ListFilesInStaging lists all files in the staging directory
func (b *Builder) ListFilesInStaging(recipe *recipe.Recipe) ([]string, error) {
stagingDir := recipe.GetStagingDir(b.workDir)
var files []string
err := filepath.Walk(stagingDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
// Convert to relative path
relPath, err := filepath.Rel(stagingDir, path)
if err != nil {
return err
}
files = append(files, relPath)
}
return nil
})
return files, err
}
// GetWorkDir returns the working directory
func (b *Builder) GetWorkDir() string {
return b.workDir
}
// SetWorkDir sets the working directory
func (b *Builder) SetWorkDir(workDir string) {
b.workDir = workDir
}
// substituteVariables substitutes variables in command strings
func (b *Builder) substituteVariables(cmdStr string, recipe *recipe.Recipe, sourceDir, stagingDir string) string {
// Get number of CPU cores
jobs := runtime.NumCPU()
// Substitute variables
cmdStr = strings.ReplaceAll(cmdStr, "${jobs}", fmt.Sprintf("%d", jobs))
cmdStr = strings.ReplaceAll(cmdStr, "${pkgdir}", stagingDir)
cmdStr = strings.ReplaceAll(cmdStr, "${srcdir}", sourceDir)
return cmdStr
}
// packageFiles runs the package commands
func (b *Builder) packageFiles(recipe *recipe.Recipe, sourceDir, stagingDir, packageDir string) error {
// Find the source directory (usually the first subdirectory)
srcPath, err := b.findSourceDirectory(sourceDir)
if err != nil {
return fmt.Errorf("failed to find source directory: %w", err)
}
// Set up environment
env := b.buildEnvironment(recipe, stagingDir)
// Execute package commands
for i, cmd := range recipe.Package.Commands {
// Substitute variables in command
cmd = b.substituteVariables(cmd, recipe, sourceDir, stagingDir)
if err := b.executeCommand(srcPath, cmd, env); err != nil {
return fmt.Errorf("package command %d failed: %w", i+1, err)
}
}
return nil
}

211
pkg/fetcher/fetcher.go Normal file
View file

@ -0,0 +1,211 @@
package fetcher
import (
"crypto/sha256"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"github.com/mholt/archiver/v3"
)
// Fetcher handles downloading and extracting sources
type Fetcher struct {
cacheDir string
}
// NewFetcher creates a new fetcher
func NewFetcher(cacheDir string) *Fetcher {
return &Fetcher{
cacheDir: cacheDir,
}
}
// Download downloads a file from URL and verifies checksum
func (f *Fetcher) Download(url, expectedHash string) (string, error) {
// Validate input parameters
if url == "" {
return "", fmt.Errorf("URL cannot be empty")
}
if expectedHash == "" {
return "", fmt.Errorf("expected hash cannot be empty")
}
filename := filepath.Base(url)
if filename == "" {
return "", fmt.Errorf("invalid URL: no filename found")
}
cachePath := filepath.Join(f.cacheDir, filename)
// Check if already downloaded and valid
if f.isValidCache(cachePath, expectedHash) {
return cachePath, nil
}
// Create cache directory if it doesn't exist
if err := os.MkdirAll(f.cacheDir, 0755); err != nil {
return "", fmt.Errorf("failed to create cache directory: %w", err)
}
// Download file
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("failed to download %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to download %s: HTTP %d", url, resp.StatusCode)
}
// Create temporary file
tmpFile, err := os.CreateTemp(f.cacheDir, "download-*")
if err != nil {
return "", fmt.Errorf("failed to create temp file: %w", err)
}
defer tmpFile.Close()
// Download to temp file
hasher := sha256.New()
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
return "", fmt.Errorf("failed to download file: %w", err)
}
// Verify checksum
calculatedHash := fmt.Sprintf("%x", hasher.Sum(nil))
if calculatedHash != expectedHash {
return "", fmt.Errorf("checksum mismatch: expected %s, got %s", expectedHash, calculatedHash)
}
// Move temp file to final location
if err := os.Rename(tmpFile.Name(), cachePath); err != nil {
return "", fmt.Errorf("failed to move downloaded file: %w", err)
}
return cachePath, nil
}
// Extract extracts an archive to destination
func (f *Fetcher) Extract(archivePath, destDir string) error {
// Create destination directory
if err := os.MkdirAll(destDir, 0755); err != nil {
return fmt.Errorf("failed to create destination directory: %w", err)
}
// Extract archive using archiver.Unarchive
return archiver.Unarchive(archivePath, destDir)
}
// isValidCache checks if cached file exists and has correct checksum
func (f *Fetcher) isValidCache(path, expectedHash string) bool {
file, err := os.Open(path)
if err != nil {
return false
}
defer file.Close()
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
return false
}
calculatedHash := fmt.Sprintf("%x", hasher.Sum(nil))
return calculatedHash == expectedHash
}
// ApplyPatches applies patch files to source directory
func (f *Fetcher) ApplyPatches(sourceDir string, patchFiles []string) error {
for _, patchFile := range patchFiles {
if err := f.applyPatch(sourceDir, patchFile); err != nil {
return fmt.Errorf("failed to apply patch %s: %w", patchFile, err)
}
}
return nil
}
// applyPatch applies a single patch file
func (f *Fetcher) applyPatch(sourceDir, patchFile string) error {
// This is a simplified patch application
// In a real implementation, you'd want to use the 'patch' command
// For now, we'll just copy the patch file to the source directory
patchDest := filepath.Join(sourceDir, filepath.Base(patchFile))
if err := copyFile(patchFile, patchDest); err != nil {
return fmt.Errorf("failed to copy patch file: %w", err)
}
return nil
}
// copyFile copies a file from src to dst
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
return err
}
// DownloadAndExtract downloads and extracts source
func (f *Fetcher) DownloadAndExtract(url, expectedHash, destDir string) error {
archivePath, err := f.Download(url, expectedHash)
if err != nil {
return err
}
return f.Extract(archivePath, destDir)
}
// DownloadMultiple downloads multiple files concurrently
func (f *Fetcher) DownloadMultiple(urls []string, hashes []string) ([]string, error) {
if len(urls) != len(hashes) {
return nil, fmt.Errorf("urls and hashes length mismatch")
}
paths := make([]string, len(urls))
for i, url := range urls {
path, err := f.Download(url, hashes[i])
if err != nil {
return nil, err
}
paths[i] = path
}
return paths, nil
}
// GetCacheDir returns the cache directory
func (f *Fetcher) GetCacheDir() string {
return f.cacheDir
}
// CleanCache removes old cached files
func (f *Fetcher) CleanCache() error {
return os.RemoveAll(f.cacheDir)
}
// ListCachedFiles lists all cached files
func (f *Fetcher) ListCachedFiles() ([]string, error) {
files, err := os.ReadDir(f.cacheDir)
if err != nil {
return nil, err
}
var result []string
for _, file := range files {
if !file.IsDir() {
result = append(result, file.Name())
}
}
return result, nil
}

404
pkg/installer/installer.go Normal file
View file

@ -0,0 +1,404 @@
package installer
import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"zsvo/pkg/packager"
"zsvo/pkg/types"
)
// Installer handles installing and removing packages
type Installer struct {
rootDir string
pkgDB string
}
// NewInstaller creates a new installer
func NewInstaller(rootDir string) *Installer {
return &Installer{
rootDir: rootDir,
pkgDB: filepath.Join(rootDir, "var", "lib", "pkgdb"),
}
}
// Install installs a package
func (i *Installer) Install(packagePath string) error {
// Extract package to temporary directory
tmpDir, err := os.MkdirTemp("", "install-")
if err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
// Extract package
pkg := packager.NewPackager(i.rootDir)
if err := pkg.Extract(packagePath, tmpDir); err != nil {
return fmt.Errorf("failed to extract package: %w", err)
}
// Read package info
pkgInfo, err := pkg.ReadPkgInfo(packagePath)
if err != nil {
return fmt.Errorf("failed to read package info: %w", err)
}
// Check dependencies
if err := i.checkDependencies(pkgInfo.Dependencies); err != nil {
return fmt.Errorf("dependency check failed: %w", err)
}
// Install files
if err := i.installFiles(tmpDir, pkgInfo); err != nil {
return fmt.Errorf("failed to install files: %w", err)
}
// Register package
if err := i.registerPackage(pkgInfo); err != nil {
return fmt.Errorf("failed to register package: %w", err)
}
return nil
}
// Remove removes a package
func (i *Installer) Remove(packageName string) error {
// Get package info
pkgInfo, err := i.getPackageInfo(packageName)
if err != nil {
return fmt.Errorf("package not found: %w", err)
}
// Remove files
if err := i.removeFiles(pkgInfo); err != nil {
return fmt.Errorf("failed to remove files: %w", err)
}
// Unregister package
if err := i.unregisterPackage(packageName); err != nil {
return fmt.Errorf("failed to unregister package: %w", err)
}
return nil
}
// ListInstalled lists all installed packages
func (i *Installer) ListInstalled() ([]string, error) {
var packages []string
// Check if pkgdb directory exists
if _, err := os.Stat(i.pkgDB); os.IsNotExist(err) {
return packages, nil
}
// Read pkgdb directory
entries, err := os.ReadDir(i.pkgDB)
if err != nil {
return nil, fmt.Errorf("failed to read package database: %w", err)
}
for _, entry := range entries {
if entry.IsDir() {
packages = append(packages, entry.Name())
}
}
return packages, nil
}
// GetPackageInfo gets information about an installed package
func (i *Installer) GetPackageInfo(packageName string) (*types.PkgInfo, error) {
return i.getPackageInfo(packageName)
}
// checkDependencies checks if all dependencies are installed
func (i *Installer) checkDependencies(deps []string) error {
installed, err := i.ListInstalled()
if err != nil {
return err
}
for _, dep := range deps {
found := false
for _, installedPkg := range installed {
if installedPkg == dep {
found = true
break
}
}
if !found {
return fmt.Errorf("dependency not installed: %s", dep)
}
}
return nil
}
// installFiles installs files from package
func (i *Installer) installFiles(tmpDir string, pkgInfo *types.PkgInfo) error {
// Validate input paths
if tmpDir == "" {
return fmt.Errorf("temporary directory cannot be empty")
}
if i.rootDir == "" {
return fmt.Errorf("root directory cannot be empty")
}
// Clean and normalize paths
tmpDir = filepath.Clean(tmpDir)
i.rootDir = filepath.Clean(i.rootDir)
// Create root directory if it doesn't exist
if err := os.MkdirAll(i.rootDir, 0755); err != nil {
return fmt.Errorf("failed to create root directory %s: %w", i.rootDir, err)
}
// Copy files to root directory
for _, file := range pkgInfo.Files {
// Validate file path
if file == "" {
return fmt.Errorf("file path cannot be empty")
}
// Clean file path to prevent directory traversal
file = filepath.Clean(file)
if strings.HasPrefix(file, "..") {
return fmt.Errorf("invalid file path: %s", file)
}
src := filepath.Join(tmpDir, file)
dst := filepath.Join(i.rootDir, file)
// Create parent directory
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", filepath.Dir(dst), err)
}
// Copy file
if err := i.copyFile(src, dst); err != nil {
return fmt.Errorf("failed to copy file %s: %w", file, err)
}
}
return nil
}
// removeFiles removes files of a package
func (i *Installer) removeFiles(pkgInfo *types.PkgInfo) error {
for _, file := range pkgInfo.Files {
path := filepath.Join(i.rootDir, file)
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove file %s: %w", file, err)
}
}
return nil
}
// registerPackage registers a package in the database
func (i *Installer) registerPackage(pkgInfo *types.PkgInfo) error {
// Create package database directory
pkgDir := filepath.Join(i.pkgDB, pkgInfo.Name)
if err := os.MkdirAll(pkgDir, 0755); err != nil {
return fmt.Errorf("failed to create package database directory: %w", err)
}
// Write package info
pkgInfoFile := filepath.Join(pkgDir, ".pkginfo")
file, err := os.Create(pkgInfoFile)
if err != nil {
return err
}
defer file.Close()
// Write package info in simple format
content := fmt.Sprintf(
"name = %q\nversion = %q\ndescription = %q\ndependencies = %v\nfiles = %v\ninstall_date = %q\n",
pkgInfo.Name, pkgInfo.Version, pkgInfo.Description, pkgInfo.Dependencies, pkgInfo.Files, pkgInfo.InstallDate,
)
_, err = file.WriteString(content)
return err
}
// unregisterPackage unregisters a package from the database
func (i *Installer) unregisterPackage(packageName string) error {
pkgDir := filepath.Join(i.pkgDB, packageName)
return os.RemoveAll(pkgDir)
}
// getPackageInfo gets package info from database
func (i *Installer) getPackageInfo(packageName string) (*types.PkgInfo, error) {
pkgDir := filepath.Join(i.pkgDB, packageName)
pkgInfoFile := filepath.Join(pkgDir, ".pkginfo")
// Check if package directory exists
if _, err := os.Stat(pkgDir); os.IsNotExist(err) {
return nil, fmt.Errorf("package %s not found in database", packageName)
}
// Check if .pkginfo file exists
if _, err := os.Stat(pkgInfoFile); os.IsNotExist(err) {
return nil, fmt.Errorf("package info file not found for %s", packageName)
}
file, err := os.Open(pkgInfoFile)
if err != nil {
return nil, err
}
defer file.Close()
var pkgInfo types.PkgInfo
// Simple parser for our package info format
// In a real implementation, you'd want to use a proper TOML parser
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
// Remove quotes from value
value = strings.Trim(value, "\"")
switch key {
case "name":
pkgInfo.Name = value
case "version":
pkgInfo.Version = value
case "description":
pkgInfo.Description = value
case "dependencies":
// Parse dependencies array
value = strings.Trim(value, "[]")
if value != "" {
pkgInfo.Dependencies = strings.Split(value, ",")
for i, dep := range pkgInfo.Dependencies {
pkgInfo.Dependencies[i] = strings.TrimSpace(strings.Trim(dep, "\""))
}
}
case "files":
// Parse files array
value = strings.Trim(value, "[]")
if value != "" {
pkgInfo.Files = strings.Split(value, ",")
for i, file := range pkgInfo.Files {
pkgInfo.Files[i] = strings.TrimSpace(strings.Trim(file, "\""))
}
}
case "install_date":
pkgInfo.InstallDate = value
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
// Validate that we actually parsed some data
if pkgInfo.Name == "" {
return nil, fmt.Errorf("invalid package info: missing name")
}
return &pkgInfo, nil
}
// copyFile copies a file from src to dst
func (i *Installer) copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
return err
}
// GetRootDir returns the root directory
func (i *Installer) GetRootDir() string {
return i.rootDir
}
// GetPkgDB returns the package database directory
func (i *Installer) GetPkgDB() string {
return i.pkgDB
}
// IsInstalled checks if a package is installed
func (i *Installer) IsInstalled(packageName string) bool {
pkgDir := filepath.Join(i.pkgDB, packageName)
_, err := os.Stat(pkgDir)
return err == nil
}
// GetInstalledVersion gets the installed version of a package
func (i *Installer) GetInstalledVersion(packageName string) (string, error) {
pkgInfo, err := i.getPackageInfo(packageName)
if err != nil {
return "", err
}
return pkgInfo.Version, nil
}
// GetInstalledFiles gets the list of installed files for a package
func (i *Installer) GetInstalledFiles(packageName string) ([]string, error) {
pkgInfo, err := i.getPackageInfo(packageName)
if err != nil {
return nil, err
}
return pkgInfo.Files, nil
}
// VerifyPackage verifies that all files of a package exist
func (i *Installer) VerifyPackage(packageName string) error {
pkgInfo, err := i.getPackageInfo(packageName)
if err != nil {
return err
}
for _, file := range pkgInfo.Files {
path := filepath.Join(i.rootDir, file)
if _, err := os.Stat(path); err != nil {
return fmt.Errorf("missing file: %s", file)
}
}
return nil
}
// GetPackageSize gets the total size of installed package files
func (i *Installer) GetPackageSize(packageName string) (int64, error) {
pkgInfo, err := i.getPackageInfo(packageName)
if err != nil {
return 0, err
}
var totalSize int64
for _, file := range pkgInfo.Files {
path := filepath.Join(i.rootDir, file)
info, err := os.Stat(path)
if err != nil {
return 0, err
}
totalSize += info.Size()
}
return totalSize, nil
}

309
pkg/packager/packager.go Normal file
View file

@ -0,0 +1,309 @@
package packager
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/mholt/archiver/v3"
"zsvo/pkg/recipe"
"zsvo/pkg/types"
)
// Packager handles creating and extracting package archives
type Packager struct {
workDir string
}
// NewPackager creates a new packager
func NewPackager(workDir string) *Packager {
return &Packager{
workDir: workDir,
}
}
// Package creates a package archive from staging directory
func (p *Packager) Package(recipe *recipe.Recipe) error {
// Validate recipe
if recipe == nil {
return fmt.Errorf("recipe cannot be nil")
}
if recipe.Name == "" {
return fmt.Errorf("recipe name cannot be empty")
}
if recipe.Version == "" {
return fmt.Errorf("recipe version cannot be empty")
}
stagingDir := recipe.GetStagingDir(p.workDir)
packageDir := recipe.GetPackageDir(p.workDir)
packageFile := filepath.Join(packageDir, recipe.GetPackageFileName())
// Validate paths
if stagingDir == "" {
return fmt.Errorf("staging directory cannot be empty")
}
if packageDir == "" {
return fmt.Errorf("package directory cannot be empty")
}
if packageFile == "" {
return fmt.Errorf("package file path cannot be empty")
}
// Clean and normalize paths
stagingDir = filepath.Clean(stagingDir)
packageDir = filepath.Clean(packageDir)
packageFile = filepath.Clean(packageFile)
// Create package directory
if err := os.MkdirAll(packageDir, 0755); err != nil {
return fmt.Errorf("failed to create package directory %s: %w", packageDir, err)
}
// List files in staging directory
files, err := p.listFiles(stagingDir)
if err != nil {
return fmt.Errorf("failed to list files in %s: %w", stagingDir, err)
}
// Create package info
pkgInfo := &types.PkgInfo{
Name: recipe.Name,
Version: recipe.Version,
Description: recipe.Description,
Dependencies: recipe.Dependencies,
Files: files,
InstallDate: time.Now().Format(time.RFC3339),
}
// Create package info file
pkgInfoFile := filepath.Join(packageDir, ".pkginfo")
if err := p.writePkgInfo(pkgInfoFile, pkgInfo); err != nil {
return fmt.Errorf("failed to write package info to %s: %w", pkgInfoFile, err)
}
// Create package archive
if err := p.createArchive(stagingDir, packageFile, pkgInfoFile); err != nil {
return fmt.Errorf("failed to create package archive %s: %w", packageFile, err)
}
return nil
}
// listFiles lists all files in a directory recursively
func (p *Packager) listFiles(dir string) ([]string, error) {
var files []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
// Convert to relative path
relPath, err := filepath.Rel(dir, path)
if err != nil {
return err
}
files = append(files, relPath)
}
return nil
})
return files, err
}
// writePkgInfo writes package info to file
func (p *Packager) writePkgInfo(path string, pkgInfo *types.PkgInfo) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
// Write package info in simple format
content := fmt.Sprintf(
"name = %q\nversion = %q\ndescription = %q\ndependencies = %v\nfiles = %v\ninstall_date = %q\n",
pkgInfo.Name, pkgInfo.Version, pkgInfo.Description, pkgInfo.Dependencies, pkgInfo.Files, pkgInfo.InstallDate,
)
_, err = file.WriteString(content)
return err
}
// createArchive creates a compressed archive
func (p *Packager) createArchive(sourceDir, archivePath, pkgInfoFile string) error {
// Create archive using archiver.Archive
paths := []string{sourceDir, pkgInfoFile}
return archiver.Archive(paths, archivePath)
}
// Extract extracts a package archive
func (p *Packager) Extract(archivePath, destDir string) error {
// Create destination directory
if err := os.MkdirAll(destDir, 0755); err != nil {
return fmt.Errorf("failed to create destination directory: %w", err)
}
// Extract archive using archiver.Unarchive
return archiver.Unarchive(archivePath, destDir)
}
// ReadPkgInfo reads package info from archive
func (p *Packager) ReadPkgInfo(archivePath string) (*types.PkgInfo, error) {
// Extract to temporary directory
tmpDir, err := os.MkdirTemp("", "pkginfo-")
if err != nil {
return nil, fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
if err := p.Extract(archivePath, tmpDir); err != nil {
return nil, fmt.Errorf("failed to extract archive: %w", err)
}
// Read package info
pkgInfoFile := filepath.Join(tmpDir, ".pkginfo")
// Check if .pkginfo file exists
if _, err := os.Stat(pkgInfoFile); os.IsNotExist(err) {
return nil, fmt.Errorf("package info file not found in archive")
}
return p.readPkgInfoFromFile(pkgInfoFile)
}
// readPkgInfoFromFile reads package info from file
func (p *Packager) readPkgInfoFromFile(path string) (*types.PkgInfo, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var pkgInfo types.PkgInfo
// Simple parser for our package info format
// In a real implementation, you'd want to use a proper TOML parser
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
// Remove quotes from value
value = strings.Trim(value, "\"")
switch key {
case "name":
pkgInfo.Name = value
case "version":
pkgInfo.Version = value
case "description":
pkgInfo.Description = value
case "dependencies":
// Parse dependencies array
value = strings.Trim(value, "[]")
if value != "" {
pkgInfo.Dependencies = strings.Split(value, ",")
for i, dep := range pkgInfo.Dependencies {
pkgInfo.Dependencies[i] = strings.TrimSpace(strings.Trim(dep, "\""))
}
}
case "files":
// Parse files array
value = strings.Trim(value, "[]")
if value != "" {
pkgInfo.Files = strings.Split(value, ",")
for i, file := range pkgInfo.Files {
pkgInfo.Files[i] = strings.TrimSpace(strings.Trim(file, "\""))
}
}
case "install_date":
pkgInfo.InstallDate = value
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
// Validate that we actually parsed some data
if pkgInfo.Name == "" {
return nil, fmt.Errorf("invalid package info: missing name")
}
return &pkgInfo, nil
}
// VerifyPackage verifies package integrity
func (p *Packager) VerifyPackage(archivePath string) error {
// Check if file exists
if _, err := os.Stat(archivePath); err != nil {
return fmt.Errorf("package file does not exist: %w", err)
}
// Try to extract to verify integrity
tmpDir, err := os.MkdirTemp("", "verify-")
if err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
return p.Extract(archivePath, tmpDir)
}
// ListPackageContents lists contents of a package
func (p *Packager) ListPackageContents(archivePath string) ([]string, error) {
// Extract to temporary directory
tmpDir, err := os.MkdirTemp("", "list-")
if err != nil {
return nil, fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
if err := p.Extract(archivePath, tmpDir); err != nil {
return nil, fmt.Errorf("failed to extract archive: %w", err)
}
// List files
return p.listFiles(tmpDir)
}
// GetPackageDir returns the package directory
func (p *Packager) GetPackageDir(recipe *recipe.Recipe) string {
return recipe.GetPackageDir(p.workDir)
}
// GetPackageFile returns the package file path
func (p *Packager) GetPackageFile(recipe *recipe.Recipe) string {
return filepath.Join(p.GetPackageDir(recipe), recipe.GetPackageFileName())
}
// Clean removes package files
func (p *Packager) Clean(recipe *recipe.Recipe) error {
packageDir := p.GetPackageDir(recipe)
return os.RemoveAll(packageDir)
}
// pkgInfoFileInfo implements os.FileInfo for package info
type pkgInfoFileInfo struct {
name string
size int64
}
func (f *pkgInfoFileInfo) Name() string { return f.name }
func (f *pkgInfoFileInfo) Size() int64 { return f.size }
func (f *pkgInfoFileInfo) Mode() os.FileMode { return 0644 }
func (f *pkgInfoFileInfo) ModTime() time.Time { return time.Now() }
func (f *pkgInfoFileInfo) IsDir() bool { return false }
func (f *pkgInfoFileInfo) Sys() interface{} { return nil }

110
pkg/recipe/recipe.go Normal file
View file

@ -0,0 +1,110 @@
package recipe
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/BurntSushi/toml"
)
// Recipe represents a package recipe
type Recipe struct {
Name string `toml:"name"`
Version string `toml:"version"`
Description string `toml:"description,omitempty"`
Source Source `toml:"source"`
Build Build `toml:"build"`
Package Package `toml:"package"`
Dependencies []string `toml:"dependencies,omitempty"`
Options map[string]bool `toml:"options,omitempty"`
}
// Source represents source information
type Source struct {
URL string `toml:"url"`
Sha256 string `toml:"sha256"`
Patches []string `toml:"patches,omitempty"`
}
// Build represents build configuration
type Build struct {
Commands []string `toml:"commands"`
Env []string `toml:"env,omitempty"`
}
// Package represents package configuration
type Package struct {
Commands []string `toml:"commands"`
}
// ParseRecipe parses a recipe from a TOML file
func ParseRecipe(path string) (*Recipe, error) {
// Validate input path
if path == "" {
return nil, fmt.Errorf("recipe path cannot be empty")
}
// Clean and normalize path
path = filepath.Clean(path)
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open recipe file %s: %w", path, err)
}
defer file.Close()
return ParseRecipeFromReader(file)
}
// ParseRecipeFromReader parses a recipe from an io.Reader
func ParseRecipeFromReader(r io.Reader) (*Recipe, error) {
var recipe Recipe
if _, err := toml.NewDecoder(r).Decode(&recipe); err != nil {
return nil, fmt.Errorf("failed to parse recipe: %w", err)
}
if recipe.Name == "" {
return nil, fmt.Errorf("recipe must have a name")
}
if recipe.Version == "" {
return nil, fmt.Errorf("recipe must have a version")
}
if recipe.Source.URL == "" {
return nil, fmt.Errorf("recipe must have a source URL")
}
if recipe.Source.Sha256 == "" {
return nil, fmt.Errorf("recipe must have a source SHA256 checksum")
}
if len(recipe.Build.Commands) == 0 {
return nil, fmt.Errorf("recipe must have build commands")
}
return &recipe, nil
}
// GetPackageName returns the package name in format name-version
func (r *Recipe) GetPackageName() string {
return fmt.Sprintf("%s-%s", r.Name, r.Version)
}
// GetPackageFileName returns the package file name
func (r *Recipe) GetPackageFileName() string {
return fmt.Sprintf("%s.pkg.tar.zst", r.GetPackageName())
}
// GetPackageDir returns the package directory path
func (r *Recipe) GetPackageDir(baseDir string) string {
return filepath.Join(baseDir, "packages", r.GetPackageName())
}
// GetSourceDir returns the source directory path
func (r *Recipe) GetSourceDir(baseDir string) string {
return filepath.Join(baseDir, "sources", r.GetPackageName())
}
// GetStagingDir returns the staging directory path
func (r *Recipe) GetStagingDir(baseDir string) string {
return filepath.Join(baseDir, "staging", r.GetPackageName())
}

11
pkg/types/pkginfo.go Normal file
View file

@ -0,0 +1,11 @@
package types
// PkgInfo represents package metadata
type PkgInfo struct {
Name string `toml:"name"`
Version string `toml:"version"`
Description string `toml:"description,omitempty"`
Dependencies []string `toml:"dependencies,omitempty"`
Files []string `toml:"files"`
InstallDate string `toml:"install_date"`
}

23
recipes/zlib.toml Normal file
View file

@ -0,0 +1,23 @@
name = "zlib"
version = "1.3"
description = "Compression library implementing the deflate compression method found in gzip and PKZIP"
[source]
url = "https://zlib.net/fossils/zlib-1.3.tar.gz"
sha256 = "ff0ba4c292013dbc27530b3a81e1f9a813cd39de01ca5e0f8bf355702efa593e"
patches = []
[build]
commands = [
"./configure --prefix=/usr --shared",
"make -j${jobs}"
]
[package]
commands = [
"make DESTDIR=${pkgdir} install"
]
env = [
"CFLAGS=-O2 -fPIC"
]

BIN
zsvo Executable file

Binary file not shown.