кал
This commit is contained in:
parent
e7c9f87cac
commit
e351ee62d1
19 changed files with 1915 additions and 2 deletions
48
.gitignore
vendored
Normal file
48
.gitignore
vendored
Normal 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
160
README.md
|
|
@ -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
46
cmd/build.go
Normal 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
51
cmd/info.go
Normal 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
39
cmd/install.go
Normal 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
44
cmd/list.go
Normal 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
39
cmd/remove.go
Normal 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
24
go.mod
Normal 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
41
go.sum
Normal 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
40
main.go
Normal 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
BIN
pkg-manager
Executable file
Binary file not shown.
317
pkg/builder/builder.go
Normal file
317
pkg/builder/builder.go
Normal 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
211
pkg/fetcher/fetcher.go
Normal 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
404
pkg/installer/installer.go
Normal 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
309
pkg/packager/packager.go
Normal 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
110
pkg/recipe/recipe.go
Normal 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
11
pkg/types/pkginfo.go
Normal 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
23
recipes/zlib.toml
Normal 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
BIN
zsvo
Executable file
Binary file not shown.
Loading…
Reference in a new issue