Compare commits
No commits in common. "fc315b1c2c2607bdc898178b259779eb89479cfc" and "d2bab3b0d1d6dc0959b67f642f02f001058892b4" have entirely different histories.
fc315b1c2c
...
d2bab3b0d1
13 changed files with 657 additions and 635 deletions
59
Dockerfile
Normal file
59
Dockerfile
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Dockerfile for ZSVO Package Manager
|
||||
# Linux-based build with build tools
|
||||
|
||||
FROM ubuntu:22.04
|
||||
|
||||
# Set non-interactive frontend for package installation
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install build tools and runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
cmake \
|
||||
meson \
|
||||
ninja-build \
|
||||
pkg-config \
|
||||
wget \
|
||||
curl \
|
||||
git \
|
||||
tar \
|
||||
gzip \
|
||||
xz-utils \
|
||||
zstd \
|
||||
python3 \
|
||||
python3-pip \
|
||||
lua5.1 \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Go
|
||||
RUN wget -O /tmp/go.tar.gz https://golang.org/dl/go1.23.0.linux-amd64.tar.gz \
|
||||
&& tar -C /usr/local -xzf /tmp/go.tar.gz \
|
||||
&& rm /tmp/go.tar.gz
|
||||
|
||||
# Set Go environment
|
||||
ENV PATH="/usr/local/go/bin:${PATH}"
|
||||
ENV GOPATH="/root/go"
|
||||
ENV GOBIN="/root/go/bin"
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build ZSVO for Linux
|
||||
RUN go build -o zsvo .
|
||||
|
||||
# Create necessary directories for ZSVO
|
||||
RUN mkdir -p /tmp/pkg-work \
|
||||
&& mkdir -p /var/lib/pkgdb \
|
||||
&& mkdir -p /var/cache/packages
|
||||
|
||||
# Make it executable
|
||||
RUN chmod +x /app/zsvo
|
||||
|
||||
# Set default command
|
||||
CMD ["/app/zsvo", "--help"]
|
||||
149
Makefile
Normal file
149
Makefile
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
# ZSVO Package Manager - Linux Build System
|
||||
# Supports Linux x86_64, ARM64, i386, ARM
|
||||
|
||||
.PHONY: help build clean test lint fmt install uninstall release docker
|
||||
|
||||
# Default target
|
||||
help: ## Show this help message
|
||||
@echo 'ZSVO Package Manager - Linux Build System'
|
||||
@echo ''
|
||||
@echo 'Usage: make [target]'
|
||||
@echo ''
|
||||
@echo 'Targets:'
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-15s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
# Variables
|
||||
BINARY_NAME=zsvo
|
||||
VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||
BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
||||
LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.buildTime=$(BUILD_TIME)"
|
||||
|
||||
# Linux platforms
|
||||
PLATFORMS=linux/amd64 linux/arm64 linux/386 linux/arm
|
||||
|
||||
# Build targets
|
||||
build: ## Build for current platform
|
||||
go build $(LDFLAGS) -o bin/$(BINARY_NAME) .
|
||||
|
||||
build-all: ## Build for all Linux platforms
|
||||
@echo "Building ZSVO for Linux platforms..."
|
||||
@mkdir -p bin
|
||||
@for platform in $(PLATFORMS); do \
|
||||
os=$$(echo $$platform | cut -d'/' -f1); \
|
||||
arch=$$(echo $$platform | cut -d'/' -f2); \
|
||||
output_name=$(BINARY_NAME)-$$arch; \
|
||||
echo "Building $$arch..."; \
|
||||
GOOS=$$os GOARCH=$$arch go build $(LDFLAGS) -o dist/$$arch/$$output_name .; \
|
||||
done
|
||||
@echo "Build complete! Binaries are in dist/"
|
||||
|
||||
# Platform-specific builds
|
||||
build-amd64: ## Build for Linux x86_64
|
||||
@echo "Building amd64..."
|
||||
@mkdir -p dist/amd64
|
||||
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o dist/amd64/$(BINARY_NAME) .
|
||||
|
||||
build-arm64: ## Build for Linux ARM64
|
||||
@echo "Building arm64..."
|
||||
@mkdir -p dist/arm64
|
||||
GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o dist/arm64/$(BINARY_NAME) .
|
||||
|
||||
build-386: ## Build for Linux i386
|
||||
@echo "Building 386..."
|
||||
@mkdir -p dist/386
|
||||
GOOS=linux GOARCH=386 go build $(LDFLAGS) -o dist/386/$(BINARY_NAME) .
|
||||
|
||||
build-arm: ## Build for Linux ARM
|
||||
@echo "Building arm..."
|
||||
@mkdir -p dist/arm
|
||||
GOOS=linux GOARCH=arm go build $(LDFLAGS) -o dist/arm/$(BINARY_NAME) .
|
||||
|
||||
# Development targets
|
||||
test: ## Run all tests
|
||||
go test -v ./pkg/...
|
||||
|
||||
test-race: ## Run tests with race detector
|
||||
go test -race -v ./pkg/...
|
||||
|
||||
test-coverage: ## Run tests with coverage
|
||||
go test -coverprofile=coverage.out ./pkg/...
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
|
||||
lint: ## Run linter
|
||||
golangci-lint run
|
||||
|
||||
fmt: ## Format code
|
||||
go fmt ./...
|
||||
goimports -w .
|
||||
|
||||
vet: ## Run go vet
|
||||
go vet ./...
|
||||
|
||||
# Installation
|
||||
install: ## Install for current platform
|
||||
go install $(LDFLAGS) .
|
||||
|
||||
uninstall: ## Uninstall
|
||||
go clean -i
|
||||
|
||||
# Distribution
|
||||
release: clean build-all ## Create release packages
|
||||
@echo "Creating release packages..."
|
||||
@mkdir -p release
|
||||
@for arch in amd64 arm64 386 arm; do \
|
||||
binary_name=$(BINARY_NAME)-$$arch; \
|
||||
release_name=$(BINARY_NAME)-$(VERSION)-$$arch; \
|
||||
release_dir=release/$$release_name; \
|
||||
mkdir -p $$release_dir; \
|
||||
cp dist/$$arch/$$binary_name $$release_dir/$(BINARY_NAME); \
|
||||
cp README.md $$release_dir/ 2>/dev/null || true; \
|
||||
cp LICENSE $$release_dir/ 2>/dev/null || true; \
|
||||
cd release && tar -czf $$release_name.tar.gz $$release_name && cd ..; \
|
||||
echo "Created $$release_name.tar.gz"; \
|
||||
done
|
||||
|
||||
# Docker
|
||||
docker-build: ## Build Docker image
|
||||
docker build -t zsvo:$(VERSION) .
|
||||
|
||||
docker-run: ## Run Docker container
|
||||
docker run -it --rm zsvo:$(VERSION)
|
||||
|
||||
# Cleanup
|
||||
clean: ## Clean build artifacts
|
||||
@echo "Cleaning build artifacts..."
|
||||
rm -rf bin/
|
||||
rm -rf dist/
|
||||
rm -rf release/
|
||||
rm -f coverage.out coverage.html
|
||||
go clean -cache
|
||||
|
||||
# Development helpers
|
||||
dev-setup: ## Setup development environment
|
||||
@echo "Setting up development environment..."
|
||||
go install golang.org/x/tools/cmd/goimports@latest
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
|
||||
pre-commit: fmt vet lint test ## Run pre-commit checks
|
||||
|
||||
# Quick build for current platform
|
||||
quick: ## Quick build for current platform (no optimizations)
|
||||
go build -o bin/$(BINARY_NAME) .
|
||||
|
||||
# Optimized build
|
||||
optimized: ## Build with optimizations for current platform
|
||||
go build -ldflags "-s -w" -o bin/$(BINARY_NAME) .
|
||||
|
||||
# Check for required tools
|
||||
check-tools:
|
||||
@echo "Checking for required tools..."
|
||||
@command -v go >/dev/null 2>&1 || { echo "Go is required but not installed."; exit 1; }
|
||||
@echo "All required tools are installed."
|
||||
|
||||
# Show build info
|
||||
info: ## Show build information
|
||||
@echo "Binary: $(BINARY_NAME)"
|
||||
@echo "Version: $(VERSION)"
|
||||
@echo "Build Time: $(BUILD_TIME)"
|
||||
@echo "Go Version: $(shell go version)"
|
||||
@echo "Supported Platforms: $(PLATFORMS)"
|
||||
14
PREBUILT.md
14
PREBUILT.md
|
|
@ -1,14 +0,0 @@
|
|||
# Pre-built packages repository
|
||||
# For users who want to install packages without building from source
|
||||
|
||||
## How to use:
|
||||
# ./zsvo install --repo-url https://github.com/yourname/zsvo-prebuilt neovim
|
||||
|
||||
## Available packages:
|
||||
- neovim-0.10.4.pkg.tar.zst # Latest neovim (built with all dependencies)
|
||||
- htop-3.4.1.pkg.tar.zst # Latest htop
|
||||
- cmake-3.28.3.pkg.tar.zst # Latest cmake
|
||||
- git-2.43.0.pkg.tar.zst # Latest git
|
||||
|
||||
## Building packages:
|
||||
# Use scripts/build-prebuilt.sh to create packages for this repository
|
||||
370
cmd/install.go
370
cmd/install.go
|
|
@ -46,7 +46,7 @@ var InstallCmd = &cobra.Command{
|
|||
status.PrintInfo(fmt.Sprintf("Work directory: %s", workDir))
|
||||
status.PrintInfo(fmt.Sprintf("Auto-source: %t", autoSource))
|
||||
status.PrintInfo(fmt.Sprintf("Auto-build-deps: %t", autoBuildDeps))
|
||||
status.PrintInfo("Auto-resolve-deps: enabled (default)")
|
||||
status.PrintInfo(fmt.Sprintf("Auto-resolve-deps: enabled (default)"))
|
||||
status.PrintFooter()
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ var InstallCmd = &cobra.Command{
|
|||
if dryRun {
|
||||
status := ui.NewStatusBar("", 1)
|
||||
status.SetTheme("neon")
|
||||
status.PrintInfo(fmt.Sprintf(i18n.T("would_install_file"), target))
|
||||
status.PrintInfo(fmt.Sprintf(i18n.T("Would install package from file: %s"), target))
|
||||
}
|
||||
installTargets = append(installTargets, target)
|
||||
continue
|
||||
|
|
@ -84,7 +84,7 @@ var InstallCmd = &cobra.Command{
|
|||
if dryRun {
|
||||
status := ui.NewStatusBar("", 1)
|
||||
status.SetTheme("neon")
|
||||
status.PrintInfo(fmt.Sprintf(i18n.T("would_auto_build"), target))
|
||||
status.PrintInfo(fmt.Sprintf(i18n.T("Would auto-build package: %s"), target))
|
||||
installTargets = append(installTargets, target) // для демонстрации
|
||||
continue
|
||||
}
|
||||
|
|
@ -100,17 +100,17 @@ var InstallCmd = &cobra.Command{
|
|||
if dryRun {
|
||||
status := ui.NewStatusBar("", 1)
|
||||
status.SetTheme("neon")
|
||||
status.PrintInfo(i18n.T("would_install_one"))
|
||||
status.PrintInfo(i18n.T("Would install 1 package"))
|
||||
} else {
|
||||
fmt.Printf(i18n.T("installing_one")+"\n", installTargets[0])
|
||||
fmt.Printf(i18n.T("Installing package from %s...")+"\n", installTargets[0])
|
||||
}
|
||||
} else {
|
||||
if dryRun {
|
||||
status := ui.NewStatusBar("", 1)
|
||||
status.SetTheme("neon")
|
||||
status.PrintInfo(fmt.Sprintf(i18n.T("would_install_many"), len(installTargets)))
|
||||
status.PrintInfo(fmt.Sprintf(i18n.T("Would install %d packages"), len(installTargets)))
|
||||
} else {
|
||||
fmt.Printf(i18n.T("installing_many")+"\n", len(installTargets))
|
||||
fmt.Printf(i18n.T("Installing %d packages...")+"\n", len(installTargets))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -184,7 +184,7 @@ func looksLikeFilePath(target string) bool {
|
|||
strings.HasSuffix(target, ".zov")
|
||||
}
|
||||
|
||||
const maxAutoBuildDepth = 3 // Уменьшим для тестов
|
||||
const maxAutoBuildDepth = 16
|
||||
|
||||
type autoBuildSession struct {
|
||||
workDir string
|
||||
|
|
@ -276,19 +276,8 @@ func (s *autoBuildSession) buildPackageWithFallback(requestName string, asBuildD
|
|||
if !allowFailure {
|
||||
return "", fmt.Errorf("failed to resolve source for %s: %w", requestName, err)
|
||||
}
|
||||
fmt.Printf("Retrying build for %s after auto-installing dependencies...\n", requestName)
|
||||
|
||||
// В LFS зависимости должны быть уже установлены
|
||||
// Показываем какие зависимости нужны для ручной установки
|
||||
missingDeps := inferMissingBuildDeps(err)
|
||||
if len(missingDeps) > 0 {
|
||||
fmt.Printf("❌ Build failed due to missing dependencies: %s\n", strings.Join(missingDeps, ", "))
|
||||
fmt.Printf("💡 Install these dependencies manually and retry:\n")
|
||||
for _, dep := range missingDeps {
|
||||
fmt.Printf(" %s\n", dep)
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("build failed - install missing dependencies manually")
|
||||
fmt.Printf("Warning: failed to resolve source for %s: %v (continuing anyway)\n", requestName, err)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
rcp := autoRecipeFromDebian(srcInfo)
|
||||
|
|
@ -301,15 +290,6 @@ func (s *autoBuildSession) buildPackageWithFallback(requestName string, asBuildD
|
|||
}
|
||||
|
||||
fmt.Printf("Building %s from %s...\n", rcp.GetPackageName(), srcInfo.DSCURL)
|
||||
|
||||
// ВАЖНО: Мы НЕ собираем Debian зависимости!
|
||||
// Мы используем Debian только как источник исходников
|
||||
// Все зависимости должны быть уже установлены в LFS системе
|
||||
if len(srcInfo.BuildDepends) > 0 {
|
||||
fmt.Printf("⚠️ Found Debian Build-Depends: %s\n", strings.Join(srcInfo.BuildDepends, ", "))
|
||||
fmt.Printf("ℹ️ In LFS, make sure these dependencies are available in your toolchain\n")
|
||||
}
|
||||
|
||||
var buildErr error
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
bar := newProgressUI(rcp.Name)
|
||||
|
|
@ -345,47 +325,10 @@ func (s *autoBuildSession) buildPackageWithFallback(requestName string, asBuildD
|
|||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return builtPackage, nil
|
||||
}
|
||||
|
||||
func (s *autoBuildSession) installSystemPackage(pkg string, currentBuildingPackage string) error {
|
||||
// In LFS environment, we need to build dependencies through zsvo
|
||||
// Try to resolve and build the package as a dependency
|
||||
fmt.Printf("Building system dependency %s through zsvo...\n", pkg)
|
||||
|
||||
// Map common Debian package names to source packages
|
||||
sourcePkg := mapDebianPackageToSource(pkg)
|
||||
if sourcePkg == "" {
|
||||
fmt.Printf("Skipping Debian-specific dependency: %s\n", pkg)
|
||||
return nil // Skip Debian-specific packages
|
||||
}
|
||||
|
||||
// Skip if this is the same package we're currently building (self-dependency)
|
||||
if sourcePkg == currentBuildingPackage {
|
||||
fmt.Printf("Skipping self-dependency: %s\n", pkg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to build the dependency
|
||||
_, err := s.buildPackageWithFallback(sourcePkg, true, false, []string{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build system dependency %s (mapped from %s): %w", sourcePkg, pkg, err)
|
||||
}
|
||||
|
||||
// Install it into the toolchain
|
||||
packagePath := filepath.Join(s.workDir, "packages", sourcePkg+".pkg.tar.zst")
|
||||
if _, err := os.Stat(packagePath); os.IsNotExist(err) {
|
||||
// Try other possible package paths
|
||||
packagePath = filepath.Join(s.workDir, sourcePkg+".pkg.tar.zst")
|
||||
}
|
||||
|
||||
if err := s.installBuildDependency(sourcePkg, packagePath); err != nil {
|
||||
return fmt.Errorf("failed to install system dependency %s: %w", sourcePkg, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *autoBuildSession) ensureBuildDependency(dep string, stack []string) error {
|
||||
dep = normalizePackageName(dep)
|
||||
if dep == "" {
|
||||
|
|
@ -403,8 +346,7 @@ func (s *autoBuildSession) ensureBuildDependency(dep string, stack []string) err
|
|||
fmt.Printf("Installing build dependency %s into %s...\n", dep, s.toolRoot)
|
||||
packagePath, err := s.buildPackageWithFallback(dep, true, false, append(stack, dep))
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: failed to build dependency %s: %v (continuing anyway)\n", dep, err)
|
||||
// Continue anyway instead of failing
|
||||
return fmt.Errorf("failed to build dependency %s: %w", dep, err)
|
||||
}
|
||||
return s.installBuildDependency(dep, packagePath)
|
||||
}
|
||||
|
|
@ -501,7 +443,6 @@ func inferMissingBuildDeps(err error) []string {
|
|||
|
||||
// Detect missing CMake
|
||||
if strings.Contains(lowerText, "cmake") &&
|
||||
!strings.Contains(lowerText, "lua 5.1") &&
|
||||
(strings.Contains(lowerText, "not found") ||
|
||||
strings.Contains(lowerText, "command not found") ||
|
||||
strings.Contains(lowerText, "cmake: command not found") ||
|
||||
|
|
@ -522,23 +463,23 @@ func inferMissingBuildDeps(err error) []string {
|
|||
found["git"] = struct{}{}
|
||||
}
|
||||
|
||||
// Detect missing make - but don't auto-add it as it's usually a build system issue
|
||||
// if strings.Contains(lowerText, "make") &&
|
||||
// (strings.Contains(lowerText, "not found") ||
|
||||
// strings.Contains(lowerText, "command not found") ||
|
||||
// strings.Contains(lowerText, "make: command not found") ||
|
||||
// strings.Contains(lowerText, "make command not found") ||
|
||||
// strings.Contains(lowerText, "no make") ||
|
||||
// strings.Contains(lowerText, "could not find make")) {
|
||||
// found["make"] = struct{}{}
|
||||
// }
|
||||
// Detect missing make
|
||||
if strings.Contains(lowerText, "make") &&
|
||||
(strings.Contains(lowerText, "not found") ||
|
||||
strings.Contains(lowerText, "command not found") ||
|
||||
strings.Contains(lowerText, "make: command not found") ||
|
||||
strings.Contains(lowerText, "make command not found") ||
|
||||
strings.Contains(lowerText, "no make") ||
|
||||
strings.Contains(lowerText, "could not find make")) {
|
||||
found["make"] = struct{}{}
|
||||
}
|
||||
|
||||
// Detect missing source files and CMake errors
|
||||
if !strings.Contains(lowerText, "lua 5.1") && (strings.Contains(lowerText, "cannot find source file") ||
|
||||
if strings.Contains(lowerText, "cannot find source file") ||
|
||||
strings.Contains(lowerText, "no sources given to target") ||
|
||||
strings.Contains(lowerText, "cmake generate step failed") ||
|
||||
strings.Contains(lowerText, "cmake error") ||
|
||||
strings.Contains(lowerText, "could not load cache")) {
|
||||
strings.Contains(lowerText, "could not load cache") {
|
||||
found["cmake"] = struct{}{}
|
||||
}
|
||||
|
||||
|
|
@ -564,225 +505,6 @@ func inferMissingBuildDeps(err error) []string {
|
|||
return deps
|
||||
}
|
||||
|
||||
func extractPackageNameFromConstraint(pkg string) string {
|
||||
// Remove version constraints like "pkg (>= 1.0)", "pkg (= 13)", "pkg <!nocheck>"
|
||||
re := regexp.MustCompile(`^([a-zA-Z0-9+.-]+)`)
|
||||
matches := re.FindStringSubmatch(pkg)
|
||||
if len(matches) > 1 {
|
||||
return matches[1]
|
||||
}
|
||||
return pkg
|
||||
}
|
||||
|
||||
func mapDebianPackageToSource(pkg string) string {
|
||||
// Extract package name from version constraints like "pkg (>= 1.0)", "pkg (= 13)"
|
||||
pkg = extractPackageNameFromConstraint(pkg)
|
||||
|
||||
// Map common Debian build dependencies to source package names
|
||||
switch pkg {
|
||||
case "cmake":
|
||||
return "cmake"
|
||||
case "pkg-config":
|
||||
return "pkgconf"
|
||||
case "build-essential":
|
||||
return "" // Skip for LFS - assume basic build tools are available
|
||||
case "dpkg-dev":
|
||||
return "" // Skip - Debian specific
|
||||
case "libc6-dev":
|
||||
return "" // Skip - basic C library should be available
|
||||
case "debhelper-compat":
|
||||
return "" // This is Debian-specific, skip for LFS
|
||||
case "debhelper":
|
||||
return "" // This is Debian-specific, skip for LFS
|
||||
case "gcc-multilib":
|
||||
return "" // Skip - Debian specific multilib
|
||||
case "directx-headers-dev":
|
||||
return "" // Skip - Windows specific
|
||||
case "libchafa-dev":
|
||||
return "" // Skip - optional dependency
|
||||
case "libddcutil-dev":
|
||||
return "" // Skip - optional dependency
|
||||
case "libdrm-dev":
|
||||
return "" // Skip - DRM specific
|
||||
case "libegl-dev":
|
||||
return "" // Skip - graphics specific
|
||||
case "libglx-dev":
|
||||
return "" // Skip - X11 specific
|
||||
case "libmagickcore-dev":
|
||||
return "" // Skip - ImageMagick specific
|
||||
case "libnm-dev":
|
||||
return "" // Skip - NetworkManager specific
|
||||
case "libosmesa6-dev":
|
||||
return "" // Skip - Mesa specific
|
||||
case "libpulse-dev":
|
||||
return "" // Skip - PulseAudio specific
|
||||
case "librpm-dev":
|
||||
return "" // Skip - RPM specific
|
||||
case "libvulkan-dev":
|
||||
return "" // Skip - Vulkan specific
|
||||
case "libwayland-dev":
|
||||
return "" // Skip - Wayland specific
|
||||
case "libxcb-randr0-dev":
|
||||
return "" // Skip - X11 specific
|
||||
case "libxfconf-0-dev":
|
||||
return "" // Skip - XFCE specific
|
||||
case "libxrandr-dev":
|
||||
return "" // Skip - X11 specific
|
||||
case "libyyjson-dev":
|
||||
return "" // Skip - already handled in build script
|
||||
case "ocl-icd-opencl-dev":
|
||||
return "" // Skip - OpenCL specific
|
||||
case "po4a":
|
||||
return "" // Skip - Debian specific
|
||||
case "help2man":
|
||||
return "" // Skip - optional
|
||||
case "dist":
|
||||
return "" // Skip - Debian specific
|
||||
case "fakeroot":
|
||||
return "" // Skip - Debian specific
|
||||
case "kyua":
|
||||
return "" // Skip - test framework
|
||||
case "atf-sh":
|
||||
return "" // Skip - test framework
|
||||
case "liblua5.1-0-dev":
|
||||
return "" // Skip - optional
|
||||
case "liblutok-dev":
|
||||
return "" // Skip - optional
|
||||
case "libsqlite3-dev":
|
||||
return "" // Skip - optional
|
||||
case "libatf-dev":
|
||||
return "" // Skip - test framework
|
||||
case "autotools-dev":
|
||||
return "" // Skip - Debian specific
|
||||
case "libmodule-build-perl":
|
||||
return "" // Skip - Perl specific
|
||||
case "sq":
|
||||
return "" // Skip - optional
|
||||
case "sqv":
|
||||
return "" // Skip - optional
|
||||
case "sqop":
|
||||
return "" // Skip - optional
|
||||
case "sqopv":
|
||||
return "" // Skip - optional
|
||||
case "rsop":
|
||||
return "" // Skip - optional
|
||||
case "rsopv":
|
||||
return "" // Skip - optional
|
||||
case "gosop":
|
||||
return "" // Skip - optional
|
||||
case "gpg-sq":
|
||||
return "" // Skip - optional
|
||||
case "gpgv-sq":
|
||||
return "" // Skip - optional
|
||||
case "gnupg":
|
||||
return "" // Skip - optional
|
||||
case "cppcheck":
|
||||
return "" // Skip - optional
|
||||
case "shellcheck":
|
||||
return "" // Skip - optional
|
||||
case "aspell":
|
||||
return "" // Skip - optional
|
||||
case "aspell-en":
|
||||
return "" // Skip - optional
|
||||
case "codespell":
|
||||
return "" // Skip - optional
|
||||
case "i18nspector":
|
||||
return "" // Skip - optional
|
||||
case "libtest-minimumversion-perl":
|
||||
return "" // Skip - test specific
|
||||
case "libtest-perl-critic-perl":
|
||||
return "" // Skip - test specific
|
||||
case "libtest-pod-coverage-perl":
|
||||
return "" // Skip - test specific
|
||||
case "libtest-pod-perl":
|
||||
return "" // Skip - test specific
|
||||
case "libtest-spelling-perl":
|
||||
return "" // Skip - test specific
|
||||
case "libtest-strict-perl":
|
||||
return "" // Skip - test specific
|
||||
case "libtest-synopsis-perl":
|
||||
return "" // Skip - test specific
|
||||
case "lcov":
|
||||
return "" // Skip - test specific
|
||||
case "libdevel-cover-perl":
|
||||
return "" // Skip - test specific
|
||||
case "procps":
|
||||
return "procps-ng"
|
||||
case "libssl-dev":
|
||||
return "openssl"
|
||||
case "libjson-c-dev":
|
||||
return "json-c"
|
||||
case "libdconf-dev":
|
||||
return "dconf"
|
||||
case "liblua5.1-dev":
|
||||
return "lua5.1"
|
||||
case "libuv1-dev":
|
||||
return "libuv"
|
||||
case "libncurses-dev":
|
||||
return "ncurses"
|
||||
case "libreadline-dev":
|
||||
return "readline"
|
||||
case "zlib1g-dev":
|
||||
return "zlib"
|
||||
case "libz-dev":
|
||||
return "zlib"
|
||||
case "libbz2-dev":
|
||||
return "bzip2"
|
||||
case "libffi-dev":
|
||||
return "libffi"
|
||||
case "libxml2-dev":
|
||||
return "libxml2"
|
||||
case "libcurl4-openssl-dev":
|
||||
return "curl"
|
||||
case "git":
|
||||
return "git"
|
||||
case "flex":
|
||||
return "flex"
|
||||
case "bison":
|
||||
return "bison"
|
||||
case "autoconf":
|
||||
return "autoconf"
|
||||
case "automake":
|
||||
return "automake"
|
||||
case "libtool":
|
||||
return "libtool"
|
||||
case "m4":
|
||||
return "m4"
|
||||
case "gettext":
|
||||
return "gettext"
|
||||
case "pkgconf":
|
||||
return "pkgconf"
|
||||
case "ninja-build":
|
||||
return "ninja"
|
||||
case "meson":
|
||||
return "meson"
|
||||
case "python3-dev":
|
||||
return "python3"
|
||||
case "libexpat1-dev":
|
||||
return "expat"
|
||||
case "libpcre3-dev":
|
||||
return "pcre3"
|
||||
case "libpcre2-dev":
|
||||
return "pcre2"
|
||||
default:
|
||||
// Try to remove -dev suffix and other common patterns
|
||||
if strings.HasSuffix(pkg, "-dev") {
|
||||
base := strings.TrimSuffix(pkg, "-dev")
|
||||
if strings.HasPrefix(base, "lib") {
|
||||
// Remove lib prefix for source packages
|
||||
base = strings.TrimPrefix(base, "lib")
|
||||
// Convert numbers like 5.1, 2.0 etc
|
||||
re := regexp.MustCompile(`[0-9]+\.[0-9]+`)
|
||||
base = re.ReplaceAllString(base, "")
|
||||
if base != "" {
|
||||
return base
|
||||
}
|
||||
}
|
||||
}
|
||||
return pkg
|
||||
}
|
||||
}
|
||||
|
||||
func mapToolToSourcePackage(tool string) string {
|
||||
tool = normalizePackageName(tool)
|
||||
if tool == "" || allDigits(tool) || len(tool) < 2 {
|
||||
|
|
@ -814,22 +536,30 @@ func mapToolToSourcePackage(tool string) string {
|
|||
return "ninja-build"
|
||||
case "python":
|
||||
return "python3"
|
||||
case "python3":
|
||||
return "python3"
|
||||
case "lua":
|
||||
return "lua5.1"
|
||||
case "luajit":
|
||||
return "luajit"
|
||||
case "cc", "c++", "g++", "gcc":
|
||||
return "gcc"
|
||||
case "ld":
|
||||
return "binutils"
|
||||
case "xzcat":
|
||||
return "xz-utils"
|
||||
case "git":
|
||||
return "git"
|
||||
case "cmake":
|
||||
return "cmake"
|
||||
case "make":
|
||||
return "make"
|
||||
case "autoconf":
|
||||
return "autoconf"
|
||||
case "automake":
|
||||
return "automake"
|
||||
case "libtool":
|
||||
return "libtool"
|
||||
case "xzcat":
|
||||
return "xz-utils"
|
||||
case "ld":
|
||||
return "binutils"
|
||||
case "git":
|
||||
return "git"
|
||||
case "cmake":
|
||||
return "cmake"
|
||||
case "pkgconf":
|
||||
return "pkgconf"
|
||||
case "flex":
|
||||
|
|
@ -921,20 +651,6 @@ func toolAlreadyAvailable(dep string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Для тестов на macOS, предположим что базовые инструменты уже доступны
|
||||
basicTools := map[string]bool{
|
||||
"gcc": true, "clang": true, "cc": true,
|
||||
"make": true, "cmake": true, "git": true,
|
||||
"pkg-config": true, "pkgconf": true,
|
||||
"flex": true, "bison": true, "m4": true,
|
||||
"autoconf": true, "automake": true, "libtool": true,
|
||||
"python3": true, "perl": true,
|
||||
}
|
||||
|
||||
if basicTools[dep] {
|
||||
return true
|
||||
}
|
||||
|
||||
commands := commandHintsByDep[dep]
|
||||
if len(commands) == 0 {
|
||||
commands = []string{dep}
|
||||
|
|
@ -973,10 +689,7 @@ func autoRecipeFromDebian(src *debian.SourceInfo) *recipe.Recipe {
|
|||
},
|
||||
Build: []string{
|
||||
"if [ ! -f configure ] && [ -f autogen.sh ]; then sh ./autogen.sh --no-check ${ZSVO_AUTOGEN_ARGS}; fi; if [ -f configure ]; then ./configure --prefix=/usr ${ZSVO_CONFIGURE_FLAGS}; fi",
|
||||
"# Download missing dependencies automatically",
|
||||
"if [ ! -f \"src/3rdparty/yyjson/yyjson.c\" ]; then echo \"Downloading yyjson...\" && mkdir -p src/3rdparty/yyjson && curl -L https://raw.githubusercontent.com/yyjson/yyjson.c/master/src/yyjson.c -o src/3rdparty/yyjson/yyjson.c; fi",
|
||||
"if [ ! -f \"src/3rdparty/yyjson/yyjson.h\" ]; then echo \"Downloading yyjson header...\" && mkdir -p src/3rdparty/yyjson && curl -L https://raw.githubusercontent.com/yyjson/yyjson.c/master/src/yyjson.h -o src/3rdparty/yyjson/yyjson.h; fi",
|
||||
"if [ -f CMakeLists.txt ]; then cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr -DUNIX=1 -DCMAKE_DISABLE_FIND_PACKAGE_Win32=1 ${ZSVO_CMAKE_FLAGS} && cmake --build build -j${jobs} -- ${ZSVO_CMAKE_BUILD_FLAGS}; " +
|
||||
"if [ -f CMakeLists.txt ]; then cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr ${ZSVO_CMAKE_FLAGS} && cmake --build build -j${jobs} -- ${ZSVO_CMAKE_BUILD_FLAGS}; " +
|
||||
"elif [ -f meson.build ]; then meson setup build --prefix=/usr ${ZSVO_MESON_SETUP_ARGS} && meson compile -C build -j${jobs} ${ZSVO_MESON_COMPILE_ARGS}; " +
|
||||
"elif [ -f Makefile ] || [ -f makefile ] || [ -f GNUmakefile ]; then make -j${jobs} ${ZSVO_MAKE_FLAGS}; fi",
|
||||
},
|
||||
|
|
@ -1100,7 +813,6 @@ func buildFailureHint(pkgName string, err error) string {
|
|||
hints,
|
||||
fmt.Sprintf("Hint: missing build dependencies detected: %s.", strings.Join(missingDeps, ", ")),
|
||||
"Hint: добавь рецепты для этих пакетов (или алиасы к Debian source), затем повтори `zsvo install`.",
|
||||
"Hint: или используйте готовые пакеты: ./zsvo install --repo-url https://github.com/yourname/zsvo-prebuilt neovim",
|
||||
)
|
||||
}
|
||||
if strings.Contains(text, "on systems using dpkg and apt, try: \"apt-get install package\"") {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,6 @@ if [ -f Makefile ] || [ -f makefile ] || [ -f GNUmakefile ]; then
|
|||
exit 0
|
||||
fi
|
||||
|
||||
# Fallback - create minimal structure
|
||||
# Fallback - create minimal structure in PKGDIR
|
||||
echo "Warning: No standard build system found, installing common directories"
|
||||
mkdir -p "$PKGDIR/usr/bin" "$PKGDIR/usr/lib" "$PKGDIR/usr/include"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
|
@ -10,7 +9,6 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"zsvo/pkg/fetcher"
|
||||
"zsvo/pkg/packager"
|
||||
|
|
@ -232,9 +230,9 @@ func (b *Builder) buildEnvironment(stagingDir string) []string {
|
|||
env := os.Environ()
|
||||
|
||||
// Add standard build environment variables
|
||||
env = append(env, "DESTDIR="+stagingDir)
|
||||
env = append(env, "PREFIX=/usr")
|
||||
env = append(env, "PKGDIR="+stagingDir)
|
||||
env = append(env, fmt.Sprintf("DESTDIR=%s", stagingDir))
|
||||
env = append(env, fmt.Sprintf("PREFIX=/usr"))
|
||||
env = append(env, fmt.Sprintf("PKGDIR=%s", stagingDir))
|
||||
|
||||
// Add parallel build variable
|
||||
env = append(env, fmt.Sprintf("MAKEFLAGS=-j%d", runtime.NumCPU()))
|
||||
|
|
@ -258,14 +256,8 @@ func (b *Builder) executeCommand(workDir, command string, env []string) error {
|
|||
cmd := exec.Command("sh", "-c", command)
|
||||
cmd.Dir = workDir
|
||||
cmd.Env = env
|
||||
if b.quiet {
|
||||
// Create a context with timeout to prevent hanging
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sh", "-c", command)
|
||||
cmd.Dir = workDir
|
||||
cmd.Env = env
|
||||
if b.quiet {
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
details := tailOutput(string(output), 20)
|
||||
|
|
@ -277,13 +269,6 @@ func (b *Builder) executeCommand(workDir, command string, env []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// For non-quiet mode, still set a reasonable timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
defer cancel()
|
||||
cmd = exec.CommandContext(ctx, "sh", "-c", command)
|
||||
cmd.Dir = workDir
|
||||
cmd.Env = env
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
|
|
@ -613,6 +598,7 @@ func (b *Builder) validateSourceFiles(recipe *recipe.Recipe, sourceDir string) e
|
|||
return nil
|
||||
}
|
||||
|
||||
// validateCommand performs basic security checks on build commands
|
||||
func (b *Builder) validateCommand(command string) error {
|
||||
// List of dangerous patterns to block
|
||||
dangerousPatterns := []string{
|
||||
|
|
@ -623,28 +609,17 @@ func (b *Builder) validateCommand(command string) error {
|
|||
"chown root",
|
||||
"sudo ",
|
||||
"su ",
|
||||
"passwd",
|
||||
"curl | sh",
|
||||
"wget | sh",
|
||||
"eval $(",
|
||||
"sh -c $(",
|
||||
"bash -c $(",
|
||||
"> /dev/sda",
|
||||
"> /dev/hda",
|
||||
"mkfs",
|
||||
"format",
|
||||
"fdisk",
|
||||
// Additional patterns
|
||||
"dd if=",
|
||||
"chmod -R 777 /",
|
||||
"chown -R root",
|
||||
"rm -rf /*",
|
||||
"rm -rf /etc",
|
||||
"rm -rf /usr",
|
||||
"rm -rf /bin",
|
||||
"rm -rf /sbin",
|
||||
"rm -rf /lib",
|
||||
"rm -rf /lib64",
|
||||
"shutdown",
|
||||
"reboot",
|
||||
"halt",
|
||||
"poweroff",
|
||||
"init 0",
|
||||
"init 6",
|
||||
}
|
||||
|
||||
cmdLower := strings.ToLower(command)
|
||||
|
|
@ -675,17 +650,6 @@ func (b *Builder) validateCommand(command string) error {
|
|||
return fmt.Errorf("command too long (%d characters)", len(command))
|
||||
}
|
||||
|
||||
// Check for command substitution patterns that could bypass security
|
||||
dangerousSubstitutions := []string{
|
||||
"`",
|
||||
"$|",
|
||||
}
|
||||
for _, pattern := range dangerousSubstitutions {
|
||||
if strings.Contains(command, pattern) {
|
||||
return fmt.Errorf("command contains potentially dangerous substitution: %s", pattern)
|
||||
}
|
||||
}
|
||||
|
||||
// Log the command for audit purposes (only first 100 chars)
|
||||
if len(command) > 100 {
|
||||
log.Printf("Command validation passed (truncated): %s...", command[:100])
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ type SourceInfo struct {
|
|||
UpstreamVersion string
|
||||
Suite string
|
||||
Component string
|
||||
BuildDepends []string // Build dependencies from DSC file
|
||||
}
|
||||
|
||||
// Resolver queries Debian source metadata over HTTP.
|
||||
|
|
@ -127,7 +126,6 @@ func (r *Resolver) ResolveSource(pkg string) (*SourceInfo, error) {
|
|||
UpstreamVersion: normalizeUpstreamVersion(record.Version),
|
||||
Suite: suite,
|
||||
Component: component,
|
||||
BuildDepends: record.BuildDepends,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
|
@ -141,13 +139,12 @@ func (r *Resolver) ResolveSource(pkg string) (*SourceInfo, error) {
|
|||
}
|
||||
|
||||
type sourceRecord struct {
|
||||
Package string
|
||||
Version string
|
||||
Directory string
|
||||
DSCName string
|
||||
DSCSHA256 string
|
||||
Binaries []string
|
||||
BuildDepends []string
|
||||
Package string
|
||||
Version string
|
||||
Directory string
|
||||
DSCName string
|
||||
DSCSHA256 string
|
||||
Binaries []string
|
||||
}
|
||||
|
||||
func (r *Resolver) findPackageInIndex(mirror, suite, component, pkg string) (*sourceRecord, error) {
|
||||
|
|
@ -293,13 +290,12 @@ func parseSourcesParagraph(lines []string) (sourceRecord, error) {
|
|||
dir = strings.Trim(strings.TrimSpace(dir), "/")
|
||||
|
||||
return sourceRecord{
|
||||
Package: pkg,
|
||||
Version: ver,
|
||||
Directory: dir,
|
||||
DSCName: dscName,
|
||||
DSCSHA256: dscHash,
|
||||
Binaries: parseCommaSeparatedField(fields["Binary"]),
|
||||
BuildDepends: parseCommaSeparatedField(fields["Build-Depends"]),
|
||||
Package: pkg,
|
||||
Version: ver,
|
||||
Directory: dir,
|
||||
DSCName: dscName,
|
||||
DSCSHA256: dscHash,
|
||||
Binaries: parseCommaSeparatedField(fields["Binary"]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package fetcher
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -12,7 +11,6 @@ import (
|
|||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/archiver/v3"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
|
@ -61,16 +59,8 @@ func (f *Fetcher) Download(url, expectedHash string) (string, error) {
|
|||
return "", fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
// Download file with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
// Download file
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to download %s: %w", url, err)
|
||||
}
|
||||
|
|
|
|||
256
pkg/i18n/i18n.go
256
pkg/i18n/i18n.go
|
|
@ -19,180 +19,172 @@ var currentLang = English
|
|||
var translations = map[Language]map[string]string{
|
||||
English: {
|
||||
// Common
|
||||
"package": "package",
|
||||
"packages": "packages",
|
||||
"building": "Building",
|
||||
"installing": "Installing",
|
||||
"removing": "Removing",
|
||||
"searching": "Searching",
|
||||
"downloading": "Downloading",
|
||||
"extracting": "Extracting",
|
||||
"configuring": "Configuring",
|
||||
"compiling": "Compiling",
|
||||
"completed": "completed",
|
||||
"failed": "failed",
|
||||
"success": "success",
|
||||
"error": "error",
|
||||
"warning": "warning",
|
||||
"info": "info",
|
||||
"package": "package",
|
||||
"packages": "packages",
|
||||
"building": "Building",
|
||||
"installing": "Installing",
|
||||
"removing": "Removing",
|
||||
"searching": "Searching",
|
||||
"downloading": "Downloading",
|
||||
"extracting": "Extracting",
|
||||
"configuring": "Configuring",
|
||||
"compiling": "Compiling",
|
||||
"completed": "completed",
|
||||
"failed": "failed",
|
||||
"success": "success",
|
||||
"error": "error",
|
||||
"warning": "warning",
|
||||
"info": "info",
|
||||
|
||||
// Commands
|
||||
"build_cmd": "Build a package from recipe",
|
||||
"install_cmd": "Install package(s)",
|
||||
"remove_cmd": "Remove installed package(s)",
|
||||
"list_cmd": "List installed packages",
|
||||
"info_cmd": "Show package information",
|
||||
"search_cmd": "Search for packages",
|
||||
"doctor_cmd": "Check system for potential issues",
|
||||
"cache_cmd": "Manage build cache",
|
||||
"build_cmd": "Build a package from recipe",
|
||||
"install_cmd": "Install package(s)",
|
||||
"remove_cmd": "Remove installed package(s)",
|
||||
"list_cmd": "List installed packages",
|
||||
"info_cmd": "Show package information",
|
||||
"search_cmd": "Search for packages",
|
||||
"doctor_cmd": "Check system for potential issues",
|
||||
"cache_cmd": "Manage build cache",
|
||||
|
||||
// Status messages
|
||||
"preparing_dirs": "Preparing directories",
|
||||
"validating_src": "Validating source files",
|
||||
"applying_patches": "Applying recipe patches",
|
||||
"creating_archive": "Creating package archive",
|
||||
"package_built": "Package %s built successfully",
|
||||
"would_install_file": "Would install package from file: %s",
|
||||
"would_auto_build": "Would auto-build package: %s",
|
||||
"would_install_one": "Would install 1 package",
|
||||
"would_install_many": "Would install %d packages",
|
||||
"installing_one": "Installing package from %s...",
|
||||
"installing_many": "Installing %d packages...",
|
||||
"preparing_dirs": "Preparing directories",
|
||||
"validating_src": "Validating source files",
|
||||
"applying_patches": "Applying recipe patches",
|
||||
"creating_archive": "Creating package archive",
|
||||
"package_built": "Package %s built successfully",
|
||||
"package_installed": "Package installation completed successfully",
|
||||
"packages_installed": "%d packages installed successfully",
|
||||
|
||||
// Progress
|
||||
"step": "Step",
|
||||
"of": "of",
|
||||
"elapsed": "elapsed",
|
||||
"remaining": "remaining",
|
||||
"eta": "ETA",
|
||||
"steps_per_sec": "steps/s",
|
||||
"step": "Step",
|
||||
"of": "of",
|
||||
"elapsed": "elapsed",
|
||||
"remaining": "remaining",
|
||||
"eta": "ETA",
|
||||
"steps_per_sec": "steps/s",
|
||||
|
||||
// Doctor
|
||||
"system_info": "System Information",
|
||||
"build_tools": "Build Tools",
|
||||
"directories": "Directories & Permissions",
|
||||
"network": "Network Connectivity",
|
||||
"package_db": "Package Database",
|
||||
"system_info": "System Information",
|
||||
"build_tools": "Build Tools",
|
||||
"directories": "Directories & Permissions",
|
||||
"network": "Network Connectivity",
|
||||
"package_db": "Package Database",
|
||||
"diagnosis_complete": "Diagnosis Complete",
|
||||
"fix_issues": "If you see any ❌ items above, fix them before using zsvo.",
|
||||
"fix_issues": "If you see any ❌ items above, fix them before using zsvo.",
|
||||
|
||||
// Cache
|
||||
"cache_info": "Build Cache Information",
|
||||
"work_dir": "Work directory",
|
||||
"total_cache": "Total cache size",
|
||||
"cache_info": "Build Cache Information",
|
||||
"work_dir": "Work directory",
|
||||
"total_cache": "Total cache size",
|
||||
"cache_breakdown": "Cache breakdown",
|
||||
"download_cache": "Download cache",
|
||||
"built_packages": "Built packages",
|
||||
"source_files": "Source files",
|
||||
"staging_files": "Staging files",
|
||||
"download_cache": "Download cache",
|
||||
"built_packages": "Built packages",
|
||||
"source_files": "Source files",
|
||||
"staging_files": "Staging files",
|
||||
"cached_packages": "Cached packages",
|
||||
"cache_cleaned": "Cache cleaned successfully",
|
||||
"cache_cleaned": "Cache cleaned successfully",
|
||||
|
||||
// Search
|
||||
"searching_for": "Searching for packages matching",
|
||||
"searching_for": "Searching for packages matching",
|
||||
"no_packages_found": "No packages found matching",
|
||||
"found_packages": "Found %d packages",
|
||||
"showing_results": "showing first %d results, use --max-results to see more",
|
||||
"package_header": "PACKAGE",
|
||||
"version_header": "VERSION",
|
||||
"desc_header": "DESCRIPTION",
|
||||
"found_packages": "Found %d packages",
|
||||
"showing_results": "showing first %d results, use --max-results to see more",
|
||||
"package_header": "PACKAGE",
|
||||
"version_header": "VERSION",
|
||||
"desc_header": "DESCRIPTION",
|
||||
|
||||
// Errors
|
||||
"recipe_not_found": "Recipe not found",
|
||||
"recipe_not_found": "Recipe not found",
|
||||
"package_not_found": "Package not found",
|
||||
"build_failed": "Build failed",
|
||||
"install_failed": "Install failed",
|
||||
"network_error": "Network error",
|
||||
"permission_error": "Permission error",
|
||||
"build_failed": "Build failed",
|
||||
"install_failed": "Install failed",
|
||||
"network_error": "Network error",
|
||||
"permission_error": "Permission error",
|
||||
},
|
||||
|
||||
Russian: {
|
||||
// Common
|
||||
"package": "кулёк",
|
||||
"packages": "кульки",
|
||||
"building": "Сборка",
|
||||
"installing": "Установка",
|
||||
"removing": "Удаление",
|
||||
"searching": "Поиск",
|
||||
"downloading": "Скачивание",
|
||||
"extracting": "Распаковка",
|
||||
"configuring": "Конфигурация",
|
||||
"compiling": "Компиляция",
|
||||
"completed": "завершено",
|
||||
"failed": "провалено",
|
||||
"success": "успешно",
|
||||
"error": "ошибка",
|
||||
"warning": "предупреждение",
|
||||
"info": "инфо",
|
||||
"package": "кулёк",
|
||||
"packages": "кульки",
|
||||
"building": "Сборка",
|
||||
"installing": "Установка",
|
||||
"removing": "Удаление",
|
||||
"searching": "Поиск",
|
||||
"downloading": "Скачивание",
|
||||
"extracting": "Распаковка",
|
||||
"configuring": "Конфигурация",
|
||||
"compiling": "Компиляция",
|
||||
"completed": "завершено",
|
||||
"failed": "провалено",
|
||||
"success": "успешно",
|
||||
"error": "ошибка",
|
||||
"warning": "предупреждение",
|
||||
"info": "инфо",
|
||||
|
||||
// Commands
|
||||
"build_cmd": "Собрать кульок из рецепта",
|
||||
"install_cmd": "Установить кульки",
|
||||
"remove_cmd": "Удалить установленные кульки",
|
||||
"list_cmd": "Показать установленные кульки",
|
||||
"info_cmd": "Информация о кульке",
|
||||
"search_cmd": "Поиск кульков",
|
||||
"doctor_cmd": "Проверка системы на проблемы",
|
||||
"cache_cmd": "Управление кэшем сборки",
|
||||
"build_cmd": "Собрать кульок из рецепта",
|
||||
"install_cmd": "Установить кульки",
|
||||
"remove_cmd": "Удалить установленные кульки",
|
||||
"list_cmd": "Показать установленные кульки",
|
||||
"info_cmd": "Информация о кульке",
|
||||
"search_cmd": "Поиск кульков",
|
||||
"doctor_cmd": "Проверка системы на проблемы",
|
||||
"cache_cmd": "Управление кэшем сборки",
|
||||
|
||||
// Status messages
|
||||
"preparing_dirs": "Подготовка директорий",
|
||||
"validating_src": "Проверка исходников",
|
||||
"applying_patches": "Применение патчей",
|
||||
"creating_archive": "Создание архива кулька",
|
||||
"package_built": "Кулёк %s успешно собран",
|
||||
"would_install_file": "Установил бы кульок из файла: %s",
|
||||
"would_auto_build": "Собрал бы кульок: %s",
|
||||
"would_install_one": "Установил бы 1 кульок",
|
||||
"would_install_many": "Установил бы %d кульков",
|
||||
"installing_one": "Устанавливаю кульок из %s...",
|
||||
"installing_many": "Устанавливаю %d кульков...",
|
||||
"preparing_dirs": "Подготовка директорий",
|
||||
"validating_src": "Проверка исходников",
|
||||
"applying_patches": "Применение патчей",
|
||||
"creating_archive": "Создание архива кулька",
|
||||
"package_built": "Кулёк %s успешно собран",
|
||||
"package_installed": "Установка кульков завершена успешно",
|
||||
"packages_installed": "%d кульков установлено успешно",
|
||||
|
||||
// Progress
|
||||
"step": "Шаг",
|
||||
"of": "из",
|
||||
"elapsed": "прошло",
|
||||
"remaining": "осталось",
|
||||
"eta": "Осталось",
|
||||
"steps_per_sec": "шагов/сек",
|
||||
"step": "Шаг",
|
||||
"of": "из",
|
||||
"elapsed": "прошло",
|
||||
"remaining": "осталось",
|
||||
"eta": "Осталось",
|
||||
"steps_per_sec": "шагов/сек",
|
||||
|
||||
// Doctor
|
||||
"system_info": "Информация о системе",
|
||||
"build_tools": "Инструменты сборки",
|
||||
"directories": "Директории и права",
|
||||
"network": "Сетевое подключение",
|
||||
"package_db": "База данных кульков",
|
||||
"system_info": "Информация о системе",
|
||||
"build_tools": "Инструменты сборки",
|
||||
"directories": "Директории и права",
|
||||
"network": "Сетевое подключение",
|
||||
"package_db": "База данных кульков",
|
||||
"diagnosis_complete": "Диагностика завершена",
|
||||
"fix_issues": "Если видишь ❌ выше, исправь перед использованием zsvo.",
|
||||
"fix_issues": "Если видишь ❌ выше, исправь перед использованием zsvo.",
|
||||
|
||||
// Cache
|
||||
"cache_info": "Информация о кэше сборки",
|
||||
"work_dir": "Рабочая директория",
|
||||
"total_cache": "Общий размер кэша",
|
||||
"cache_info": "Информация о кэше сборки",
|
||||
"work_dir": "Рабочая директория",
|
||||
"total_cache": "Общий размер кэша",
|
||||
"cache_breakdown": "Детализация кэша",
|
||||
"download_cache": "Кэш загрузок",
|
||||
"built_packages": "Собранные кульки",
|
||||
"source_files": "Исходники",
|
||||
"staging_files": "Временные файлы",
|
||||
"download_cache": "Кэш загрузок",
|
||||
"built_packages": "Собранные кульки",
|
||||
"source_files": "Исходники",
|
||||
"staging_files": "Временные файлы",
|
||||
"cached_packages": "Закэшированные кульки",
|
||||
"cache_cleaned": "Кэш очищен успешно",
|
||||
"cache_cleaned": "Кэш очищен успешно",
|
||||
|
||||
// Search
|
||||
"searching_for": "Поиск кульков по запросу",
|
||||
"searching_for": "Поиск кульков по запросу",
|
||||
"no_packages_found": "Кульки не найдены по запросу",
|
||||
"found_packages": "Найдено кульков: %d",
|
||||
"showing_results": "показано первых %d, используй --max-results для больше",
|
||||
"package_header": "КУЛЁК",
|
||||
"version_header": "ВЕРСИЯ",
|
||||
"desc_header": "ОПИСАНИЕ",
|
||||
"found_packages": "Найдено кульков: %d",
|
||||
"showing_results": "показано первых %d, используй --max-results для больше",
|
||||
"package_header": "КУЛЁК",
|
||||
"version_header": "ВЕРСИЯ",
|
||||
"desc_header": "ОПИСАНИЕ",
|
||||
|
||||
// Errors
|
||||
"recipe_not_found": "Рецепт не найден",
|
||||
"recipe_not_found": "Рецепт не найден",
|
||||
"package_not_found": "Кулёк не найден",
|
||||
"build_failed": "Сборка провалена",
|
||||
"install_failed": "Установка провалена",
|
||||
"network_error": "Ошибка сети",
|
||||
"permission_error": "Ошибка прав доступа",
|
||||
"build_failed": "Сборка провалена",
|
||||
"install_failed": "Установка провалена",
|
||||
"network_error": "Ошибка сети",
|
||||
"permission_error": "Ошибка прав доступа",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -255,13 +255,6 @@ func (i *Installer) installPackageTxNoLock(packagePath, txDir string, installedI
|
|||
return nil, fmt.Errorf("installed infos map cannot be nil")
|
||||
}
|
||||
|
||||
// Validate package file exists and is readable
|
||||
if _, err := os.Stat(packagePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("package file does not exist: %s", packagePath)
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("cannot access package file %s: %w", packagePath, err)
|
||||
}
|
||||
|
||||
pkgTxDir, err := os.MkdirTemp(txDir, "pkg-")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create package transaction directory: %w", err)
|
||||
|
|
@ -307,20 +300,8 @@ func (i *Installer) installPackageTxNoLock(packagePath, txDir string, installedI
|
|||
return nil, fmt.Errorf("failed to remove old package %s files: %w", pkgInfo.Name, err)
|
||||
}
|
||||
if err := i.unregisterPackage(pkgInfo.Name); err != nil {
|
||||
// Attempt to rollback, but aggregate errors
|
||||
var rollbackErrs []string
|
||||
if rbErr := i.rollbackRemove(backups); rbErr != nil {
|
||||
rollbackErrs = append(rollbackErrs, fmt.Sprintf("rollback error: %v", rbErr))
|
||||
}
|
||||
if rbErr := i.registerPackage(oldInfo); rbErr != nil {
|
||||
rollbackErrs = append(rollbackErrs, fmt.Sprintf("reregister error: %v", rbErr))
|
||||
}
|
||||
|
||||
errMsg := fmt.Sprintf("failed to unregister old package %s: %v", pkgInfo.Name, err)
|
||||
if len(rollbackErrs) > 0 {
|
||||
errMsg += fmt.Sprintf(" (rollback errors: %s)", strings.Join(rollbackErrs, "; "))
|
||||
}
|
||||
return nil, fmt.Errorf(errMsg)
|
||||
_ = i.rollbackRemove(backups)
|
||||
return nil, fmt.Errorf("failed to unregister old package %s: %w", pkgInfo.Name, err)
|
||||
}
|
||||
|
||||
state.replaced = &removeTransactionState{
|
||||
|
|
@ -333,49 +314,23 @@ func (i *Installer) installPackageTxNoLock(packagePath, txDir string, installedI
|
|||
backupRoot := filepath.Join(pkgTxDir, "new")
|
||||
installedPaths, backups, err := i.installFiles(extractRoot, pkgInfo, backupRoot)
|
||||
if err != nil {
|
||||
// Attempt rollback with error aggregation
|
||||
var rollbackErrs []string
|
||||
if rbErr := i.rollbackInstall(installedPaths, backups); rbErr != nil {
|
||||
rollbackErrs = append(rollbackErrs, fmt.Sprintf("install rollback error: %v", rbErr))
|
||||
}
|
||||
_ = i.rollbackInstall(installedPaths, backups)
|
||||
if state.replaced != nil {
|
||||
if rbErr := i.rollbackRemove(state.replaced.backups); rbErr != nil {
|
||||
rollbackErrs = append(rollbackErrs, fmt.Sprintf("remove rollback error: %v", rbErr))
|
||||
}
|
||||
if rbErr := i.registerPackage(state.replaced.info); rbErr != nil {
|
||||
rollbackErrs = append(rollbackErrs, fmt.Sprintf("reregister error: %v", rbErr))
|
||||
}
|
||||
_ = i.rollbackRemove(state.replaced.backups)
|
||||
_ = i.registerPackage(state.replaced.info)
|
||||
}
|
||||
|
||||
errMsg := fmt.Sprintf("failed to install files: %v", err)
|
||||
if len(rollbackErrs) > 0 {
|
||||
errMsg += fmt.Sprintf(" (rollback errors: %s)", strings.Join(rollbackErrs, "; "))
|
||||
}
|
||||
return nil, fmt.Errorf(errMsg)
|
||||
return nil, fmt.Errorf("failed to install files: %w", err)
|
||||
}
|
||||
state.installedPaths = installedPaths
|
||||
state.newBackups = backups
|
||||
|
||||
if err := i.registerPackage(pkgInfo); err != nil {
|
||||
// Attempt rollback with error aggregation
|
||||
var rollbackErrs []string
|
||||
if rbErr := i.rollbackInstall(installedPaths, backups); rbErr != nil {
|
||||
rollbackErrs = append(rollbackErrs, fmt.Sprintf("install rollback error: %v", rbErr))
|
||||
}
|
||||
_ = i.rollbackInstall(installedPaths, backups)
|
||||
if state.replaced != nil {
|
||||
if rbErr := i.rollbackRemove(state.replaced.backups); rbErr != nil {
|
||||
rollbackErrs = append(rollbackErrs, fmt.Sprintf("remove rollback error: %v", rbErr))
|
||||
}
|
||||
if rbErr := i.registerPackage(state.replaced.info); rbErr != nil {
|
||||
rollbackErrs = append(rollbackErrs, fmt.Sprintf("reregister error: %v", rbErr))
|
||||
}
|
||||
_ = i.rollbackRemove(state.replaced.backups)
|
||||
_ = i.registerPackage(state.replaced.info)
|
||||
}
|
||||
|
||||
errMsg := fmt.Sprintf("failed to register package: %v", err)
|
||||
if len(rollbackErrs) > 0 {
|
||||
errMsg += fmt.Sprintf(" (rollback errors: %s)", strings.Join(rollbackErrs, "; "))
|
||||
}
|
||||
return nil, fmt.Errorf(errMsg)
|
||||
return nil, fmt.Errorf("failed to register package: %w", err)
|
||||
}
|
||||
|
||||
return state, nil
|
||||
|
|
@ -1319,13 +1274,9 @@ func (i *Installer) withDBLock(exclusive bool, fn func() error) error {
|
|||
lockType = syscall.LOCK_EX
|
||||
}
|
||||
if err := syscall.Flock(int(lockFile.Fd()), lockType); err != nil {
|
||||
lockFile.Close()
|
||||
return fmt.Errorf("failed to lock package database: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
syscall.Flock(int(lockFile.Fd()), syscall.LOCK_UN)
|
||||
lockFile.Close()
|
||||
}()
|
||||
defer syscall.Flock(int(lockFile.Fd()), syscall.LOCK_UN)
|
||||
|
||||
return fn()
|
||||
}
|
||||
|
|
|
|||
142
scripts/docker.sh
Executable file
142
scripts/docker.sh
Executable file
|
|
@ -0,0 +1,142 @@
|
|||
#!/bin/bash
|
||||
|
||||
# ZSVO Docker Build Script
|
||||
# Linux-only Docker builds
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
IMAGE_NAME="zsvo"
|
||||
VERSION=${VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo "dev")}
|
||||
REGISTRY=${REGISTRY:-"localhost:5000"}
|
||||
|
||||
# Functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
check_docker() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
log_error "Docker is not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker info &> /dev/null; then
|
||||
log_error "Docker daemon is not running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "Docker check passed"
|
||||
}
|
||||
|
||||
build_image() {
|
||||
local tag=${1:-$IMAGE_NAME:$VERSION}
|
||||
|
||||
log_info "Building Docker image..."
|
||||
docker build -t $tag .
|
||||
log_success "Built Docker image: $tag"
|
||||
}
|
||||
|
||||
run_container() {
|
||||
local tag=${1:-$IMAGE_NAME:$VERSION}
|
||||
local command=${2:-"--help"}
|
||||
|
||||
log_info "Running container: $tag"
|
||||
log_info "Command: zsvo $command"
|
||||
docker run --rm -it $tag zsvo $command
|
||||
}
|
||||
|
||||
push_image() {
|
||||
local tag=${1:-$IMAGE_NAME:$VERSION}
|
||||
local remote=${2:-$REGISTRY}
|
||||
|
||||
log_info "Pushing image: $tag"
|
||||
docker tag $tag $remote/$tag
|
||||
docker push $remote/$tag
|
||||
log_success "Image pushed: $remote/$tag"
|
||||
}
|
||||
|
||||
clean_images() {
|
||||
log_info "Cleaning Docker images..."
|
||||
|
||||
docker images $IMAGE_NAME --format "table {{.Repository}}:{{.Tag}}" | grep -v REPOSITORY | while read line; do
|
||||
if [ -n "$line" ]; then
|
||||
docker rmi $line 2>/dev/null || true
|
||||
log_info "Removed: $line"
|
||||
fi
|
||||
done
|
||||
|
||||
log_success "Docker cleanup complete"
|
||||
}
|
||||
|
||||
show_info() {
|
||||
log_info "ZSVO Docker Information:"
|
||||
echo " Image: $IMAGE_NAME"
|
||||
echo " Version: $VERSION"
|
||||
echo " Registry: $REGISTRY"
|
||||
echo ""
|
||||
echo "Available images:"
|
||||
docker images $IMAGE_NAME 2>/dev/null | head -10 || echo " No images found"
|
||||
}
|
||||
|
||||
# Main script
|
||||
case "${1:-help}" in
|
||||
"build")
|
||||
build_image "$2"
|
||||
;;
|
||||
"run")
|
||||
run_container "$2" "$3"
|
||||
;;
|
||||
"push")
|
||||
push_image "$2" "$3"
|
||||
;;
|
||||
"clean")
|
||||
clean_images
|
||||
;;
|
||||
"info")
|
||||
show_info
|
||||
;;
|
||||
"help"|*)
|
||||
echo "ZSVO Docker Build Script"
|
||||
echo ""
|
||||
echo "Usage: $0 [command] [options]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " build [tag] Build Docker image"
|
||||
echo " run [tag] [command] Run container"
|
||||
echo " push [tag] [registry] Push image to registry"
|
||||
echo " clean Clean Docker images"
|
||||
echo " info Show Docker information"
|
||||
echo " help Show this help"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 build"
|
||||
echo " $0 build zsvo:latest"
|
||||
echo " $0 run zsvo:latest --help"
|
||||
echo " $0 push zsvo:latest my-registry.com"
|
||||
echo ""
|
||||
echo "Environment Variables:"
|
||||
echo " VERSION Override version"
|
||||
echo " REGISTRY Docker registry"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
41
test-env.dockerfile
Normal file
41
test-env.dockerfile
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Test environment for ZSVO on Debian 13
|
||||
FROM debian:13
|
||||
|
||||
# Install basic build tools
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
cmake \
|
||||
git \
|
||||
make \
|
||||
pkg-config \
|
||||
wget \
|
||||
curl \
|
||||
golang-go \
|
||||
sudo \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create zsvo user with sudo access
|
||||
RUN useradd -m -s /bin/bash -G sudo zsvo
|
||||
RUN echo "zsvo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /home/zsvo
|
||||
|
||||
# Copy zsvo binary and install script
|
||||
COPY zsvo /usr/local/bin/zsvo
|
||||
COPY install-script.sh /tmp/install-script.sh
|
||||
RUN chmod +x /usr/local/bin/zsvo /tmp/install-script.sh
|
||||
|
||||
# Create necessary directories with proper permissions
|
||||
RUN mkdir -p /tmp/pkg-work /home/zsvo/packages /var/lib/pkgdb && \
|
||||
chown -R zsvo:zsvo /tmp/pkg-work /home/zsvo/packages /var/lib/pkgdb
|
||||
|
||||
# Switch to zsvo user
|
||||
USER zsvo
|
||||
|
||||
# Set environment
|
||||
ENV PATH=/usr/local/bin:/usr/bin:/bin
|
||||
ENV HOME=/home/zsvo
|
||||
|
||||
# Test command
|
||||
CMD ["/bin/bash"]
|
||||
40
zsvo-docker.sh
Executable file
40
zsvo-docker.sh
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "🚀 Запускаем ZSVO в Docker..."
|
||||
|
||||
# Проверяем что Docker запущен
|
||||
if ! docker ps > /dev/null 2>&1; then
|
||||
echo "❌ Docker не запущен. Запускаем Docker Desktop..."
|
||||
open -a Docker
|
||||
echo "⏳ Ждем запуска Docker..."
|
||||
sleep 10
|
||||
fi
|
||||
|
||||
# Запускаем контейнер
|
||||
docker run -it --rm \
|
||||
-v $(pwd):/workspace \
|
||||
-w /workspace \
|
||||
--name zsvo-test \
|
||||
golang:1.23 bash -c "
|
||||
echo '=== УСТАНАВЛИВАЕМ ЗАВИСИМОСТИ ===' &&
|
||||
apt-get update -qq &&
|
||||
apt-get install -y -qq build-essential git curl wget cmake meson pkg-config python3 tar xz-utils &&
|
||||
|
||||
echo '=== СОБИРАЕМ ZSVO ===' &&
|
||||
go build -o zsvo . &&
|
||||
|
||||
echo '=== ГОТОВО! ТЕРМИНАЛ ZSVO ===' &&
|
||||
echo 'Доступные команды:' &&
|
||||
echo ' ./zsvo doctor' &&
|
||||
echo ' ./zsvo install htop --dry-run' &&
|
||||
echo ' ./zsvo install neovim --dry-run' &&
|
||||
echo ' ./zsvo search python' &&
|
||||
echo ' ./zsvo lang ru' &&
|
||||
echo ' ./zsvo cache info' &&
|
||||
echo '' &&
|
||||
echo 'Для выхода: exit' &&
|
||||
echo '' &&
|
||||
bash
|
||||
"
|
||||
|
||||
echo "✅ ZSVO Docker контейнер завершен"
|
||||
Loading…
Reference in a new issue