This commit is contained in:
itexpert228 2026-03-14 22:03:19 +03:00
parent 98adef8fe7
commit f07f62244c
No known key found for this signature in database
3 changed files with 313 additions and 62 deletions

View file

@ -186,7 +186,8 @@ func looksLikeFilePath(target string) bool {
} }
const maxAutoBuildDepth = 15 // Увеличим для сложных цепочек зависимостей const maxAutoBuildDepth = 15 // Увеличим для сложных цепочек зависимостей
const parallelWorkers = 4 // Количество параллельных воркеров для сборки const parallelWorkers = 20 // Параллельный поиск зависимостей (HTTP)
const buildWorkers = 4 // Параллельное построение пакетов (CPU intensive)
type autoBuildSession struct { type autoBuildSession struct {
workDir string workDir string
@ -641,6 +642,7 @@ func (s *autoBuildSession) installBuildDependency(dep, packagePath string) error
} }
// collectAllDependencies recursively collects all dependencies into a graph without building // collectAllDependencies recursively collects all dependencies into a graph without building
// Now uses parallel workers (200 concurrent) for faster dependency resolution
func (s *autoBuildSession) collectAllDependencies(rootPkg string, graph *depGraph, stack []string) error { func (s *autoBuildSession) collectAllDependencies(rootPkg string, graph *depGraph, stack []string) error {
rootPkg = normalizePackageName(rootPkg) rootPkg = normalizePackageName(rootPkg)
if rootPkg == "" { if rootPkg == "" {
@ -679,7 +681,8 @@ func (s *autoBuildSession) collectAllDependencies(rootPkg string, graph *depGrap
node.recipe = rcp node.recipe = rcp
node.buildDepends = srcInfo.BuildDepends node.buildDepends = srcInfo.BuildDepends
// Process build dependencies // Collect all dependency names first
var depsToProcess []string
for _, dep := range srcInfo.BuildDepends { for _, dep := range srcInfo.BuildDepends {
depName := extractPackageNameFromConstraint(dep) depName := extractPackageNameFromConstraint(dep)
if depName == "" { if depName == "" {
@ -698,10 +701,41 @@ func (s *autoBuildSession) collectAllDependencies(rootPkg string, graph *depGrap
// Add dependency relationship // Add dependency relationship
graph.addDependency(pkgName, sourcePkg) graph.addDependency(pkgName, sourcePkg)
// Recursively collect this dependency's dependencies // Check if already in graph
if err := s.collectAllDependencies(sourcePkg, graph, append(stack, pkgName)); err != nil { depNode := graph.getOrCreateNode(sourcePkg)
// Log but don't fail - some deps might be optional if depNode.srcInfo == nil {
fmt.Printf(" ⚠️ Could not collect dependency %s: %v\n", sourcePkg, err) depsToProcess = append(depsToProcess, sourcePkg)
}
}
// Process dependencies in parallel using worker pool
if len(depsToProcess) > 0 {
var wg sync.WaitGroup
errChan := make(chan error, len(depsToProcess))
// Semaphore to limit concurrent workers (200 workers)
semaphore := make(chan struct{}, parallelWorkers)
for _, dep := range depsToProcess {
wg.Add(1)
go func(depName string) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
if err := s.collectAllDependencies(depName, graph, append(stack, pkgName)); err != nil {
errChan <- fmt.Errorf("could not collect dependency %s: %w", depName, err)
}
}(dep)
}
wg.Wait()
close(errChan)
// Log errors but don't fail
for err := range errChan {
fmt.Printf(" ⚠️ %v\n", err)
} }
} }
@ -749,7 +783,7 @@ func (s *autoBuildSession) buildDependenciesParallel(graph *depGraph) error {
errChan := make(chan error, len(nodesToBuild)) errChan := make(chan error, len(nodesToBuild))
// Limit concurrent builds // Limit concurrent builds
semaphore := make(chan struct{}, parallelWorkers) semaphore := make(chan struct{}, buildWorkers)
for _, node := range nodesToBuild { for _, node := range nodesToBuild {
wg.Add(1) wg.Add(1)
@ -857,6 +891,7 @@ func joinPathListUnique(parts []string) string {
} }
var simplePkgNamePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9+.-]*[a-z0-9]$`) var simplePkgNamePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9+.-]*[a-z0-9]$`)
var versionNumberPattern = regexp.MustCompile(`[0-9]+\.[0-9]+`)
var missingCommandPatterns = []*regexp.Regexp{ var missingCommandPatterns = []*regexp.Regexp{
regexp.MustCompile(`(?m)(?:^|[\s:])(?:/bin/)?sh:\s*(?:\d+:\s*)?([a-zA-Z0-9+_.-]+):\s*(?:command not found|not found)\b`), regexp.MustCompile(`(?m)(?:^|[\s:])(?:/bin/)?sh:\s*(?:\d+:\s*)?([a-zA-Z0-9+_.-]+):\s*(?:command not found|not found)\b`),
regexp.MustCompile(`(?m)\b([a-zA-Z0-9+_.-]+):\s*command not found\b`), regexp.MustCompile(`(?m)\b([a-zA-Z0-9+_.-]+):\s*command not found\b`),
@ -1171,8 +1206,7 @@ func mapDebianPackageToSource(pkg string) string {
// Remove lib prefix for source packages // Remove lib prefix for source packages
base = strings.TrimPrefix(base, "lib") base = strings.TrimPrefix(base, "lib")
// Convert numbers like 5.1, 2.0 etc // Convert numbers like 5.1, 2.0 etc
re := regexp.MustCompile(`[0-9]+\.[0-9]+`) base = versionNumberPattern.ReplaceAllString(base, "")
base = re.ReplaceAllString(base, "")
if base != "" { if base != "" {
return base return base
} }
@ -1320,20 +1354,7 @@ func toolAlreadyAvailable(dep string) bool {
return false return false
} }
// Для тестов на macOS, предположим что базовые инструменты уже доступны // Check if command exists in PATH
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] commands := commandHintsByDep[dep]
if len(commands) == 0 { if len(commands) == 0 {
commands = []string{dep} commands = []string{dep}

View file

@ -255,9 +255,6 @@ func (b *Builder) executeCommand(workDir, command string, env []string) error {
log.Printf("Executing command: %s", command) log.Printf("Executing command: %s", command)
cmd := exec.Command("sh", "-c", command)
cmd.Dir = workDir
cmd.Env = env
if b.quiet { if b.quiet {
// Create a context with timeout to prevent hanging - use 2 hours for large packages like gcc/llvm // Create a context with timeout to prevent hanging - use 2 hours for large packages like gcc/llvm
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
@ -280,7 +277,7 @@ func (b *Builder) executeCommand(workDir, command string, env []string) error {
// For non-quiet mode, still set a reasonable timeout - use 2 hours for large packages // For non-quiet mode, still set a reasonable timeout - use 2 hours for large packages
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
defer cancel() defer cancel()
cmd = exec.CommandContext(ctx, "sh", "-c", command) cmd := exec.CommandContext(ctx, "sh", "-c", command)
cmd.Dir = workDir cmd.Dir = workDir
cmd.Env = env cmd.Env = env

View file

@ -6,9 +6,12 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os"
"path" "path"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"sync"
"time" "time"
"github.com/ulikunitz/xz" "github.com/ulikunitz/xz"
@ -19,8 +22,8 @@ var packageNamePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9+.-]*$`)
const defaultDebianMirror = "https://deb.debian.org/debian" const defaultDebianMirror = "https://deb.debian.org/debian"
var ( var (
defaultSuites = []string{"stable", "testing", "unstable"} defaultSuites = []string{"stable"} // Только stable для скорости
defaultComponents = []string{"main", "contrib", "non-free", "non-free-firmware"} defaultComponents = []string{"main"} // Только main для скорости
) )
// SourceInfo describes Debian source package coordinates resolved over HTTP. // SourceInfo describes Debian source package coordinates resolved over HTTP.
@ -37,14 +40,26 @@ type SourceInfo struct {
} }
const maxAutoBuildDepth = 15 // Увеличим для сложных цепочек зависимостей const maxAutoBuildDepth = 15 // Увеличим для сложных цепочек зависимостей
const parallelWorkers = 1 // Временно отключаем параллельную сборку (1 воркер = последовательно) const parallelWorkers = 20 // Параллельный поиск зависимостей (HTTP запросы)
const buildWorkers = 4 // Параллельное построение пакетов
// CachedSources holds parsed Sources data for fast lookups
type CachedSources struct {
packages map[string]*sourceRecord // key: package name
path string // cache file path
mu sync.RWMutex
}
// Resolver queries Debian source metadata over HTTP. // Resolver queries Debian source metadata over HTTP.
type Resolver struct { type Resolver struct {
client *http.Client client *http.Client
mirrors []string mirrors []string
suites []string suites []string
components []string components []string
cache map[string]*SourceInfo // Кеш найденных пакетов
cacheMu sync.RWMutex // Защита кеша
cachedSources map[string]*CachedSources // Кешированные Sources файлы по ключу
sourcesMu sync.RWMutex // Защита cachedSources map
} }
// ResolverOption customizes resolver behavior. // ResolverOption customizes resolver behavior.
@ -87,10 +102,12 @@ func NewResolver(opts ...ResolverOption) *Resolver {
}, },
}, },
mirrors: []string{ mirrors: []string{
defaultDebianMirror, defaultDebianMirror, // deb.debian.org (CDN)
}, },
suites: append([]string(nil), defaultSuites...), suites: append([]string(nil), defaultSuites...),
components: append([]string(nil), defaultComponents...), components: append([]string(nil), defaultComponents...),
cache: make(map[string]*SourceInfo),
cachedSources: make(map[string]*CachedSources),
} }
for _, opt := range opts { for _, opt := range opts {
@ -118,6 +135,14 @@ func (r *Resolver) ResolveSource(pkg string) (*SourceInfo, error) {
return nil, fmt.Errorf("invalid package name %q", pkg) return nil, fmt.Errorf("invalid package name %q", pkg)
} }
// Проверяем кеш
r.cacheMu.RLock()
if cached, ok := r.cache[pkg]; ok {
r.cacheMu.RUnlock()
return cached, nil
}
r.cacheMu.RUnlock()
fmt.Printf(" [resolver] Looking up %s...\n", pkg) fmt.Printf(" [resolver] Looking up %s...\n", pkg)
start := time.Now() start := time.Now()
defer func() { defer func() {
@ -136,8 +161,7 @@ func (r *Resolver) ResolveSource(pkg string) (*SourceInfo, error) {
continue continue
} }
fmt.Printf(" [resolver] ✓ Found %s in %s/%s/%s\n", pkg, mirror, suite, component) result := &SourceInfo{
return &SourceInfo{
RequestedPackage: pkg, RequestedPackage: pkg,
SourcePackage: record.Package, SourcePackage: record.Package,
DSCURL: strings.TrimRight(mirror, "/") + "/" + path.Join(record.Directory, record.DSCName), DSCURL: strings.TrimRight(mirror, "/") + "/" + path.Join(record.Directory, record.DSCName),
@ -147,7 +171,15 @@ func (r *Resolver) ResolveSource(pkg string) (*SourceInfo, error) {
Suite: suite, Suite: suite,
Component: component, Component: component,
BuildDepends: record.BuildDepends, BuildDepends: record.BuildDepends,
}, nil }
// Сохраняем в кеш
r.cacheMu.Lock()
r.cache[pkg] = result
r.cacheMu.Unlock()
fmt.Printf(" [resolver] ✓ Found %s in %s/%s/%s\n", pkg, mirror, suite, component)
return result, nil
} }
} }
} }
@ -170,31 +202,18 @@ type sourceRecord struct {
} }
func (r *Resolver) findPackageInIndex(mirror, suite, component, pkg string) (*sourceRecord, error) { func (r *Resolver) findPackageInIndex(mirror, suite, component, pkg string) (*sourceRecord, error) {
variants := []struct { // First, ensure Sources file is loaded into memory
ext string cached, err := r.loadCachedSources(mirror, suite, component)
decoder func(io.Reader) (io.Reader, error) if err != nil {
}{ return nil, err
{ext: "xz", decoder: decodeXZ},
{ext: "gz", decoder: decodeGzip},
{ext: "", decoder: passthrough},
} }
var firstErr error // Lookup from in-memory map (O(1) instead of HTTP scan)
for _, v := range variants { if rec, found := r.lookupFromCache(pkg, cached); found {
indexURL := buildSourcesURL(mirror, suite, component, v.ext) return rec, nil
record, err := r.findInSingleIndex(indexURL, v.decoder, pkg)
if err == nil {
return record, nil
}
if firstErr == nil {
firstErr = err
}
} }
if firstErr == nil { return nil, fmt.Errorf("package not found in cache")
firstErr = fmt.Errorf("could not read sources index")
}
return nil, firstErr
} }
func (r *Resolver) findInSingleIndex(indexURL string, decoder func(io.Reader) (io.Reader, error), pkg string) (*sourceRecord, error) { func (r *Resolver) findInSingleIndex(indexURL string, decoder func(io.Reader) (io.Reader, error), pkg string) (*sourceRecord, error) {
@ -389,7 +408,9 @@ func normalizeUpstreamVersion(debianVersion string) string {
v = v[idx+1:] v = v[idx+1:]
} }
if idx := strings.LastIndex(v, "-"); idx > 0 { // Find the last dash that separates upstream version from Debian revision
// For versions like "1.0-1-ubuntu1", we want to cut at the first dash to get "1.0"
if idx := strings.Index(v, "-"); idx >= 0 {
v = v[:idx] v = v[:idx]
} }
@ -421,3 +442,215 @@ func normalizeList(in []string) []string {
} }
return out return out
} }
// cacheDir returns the cache directory for zsvo
func cacheDir() string {
if dir := os.Getenv("ZSVO_CACHE"); dir != "" {
return dir
}
if home, err := os.UserHomeDir(); err == nil {
return filepath.Join(home, ".cache", "zsvo")
}
return "/var/cache/zsvo"
}
// ensureCacheDir creates the cache directory if it doesn't exist
func ensureCacheDir() error {
dir := cacheDir()
return os.MkdirAll(dir, 0755)
}
// cachePath returns the path for a cached Sources file
func (r *Resolver) cachePath(mirror, suite, component string) string {
host := strings.ReplaceAll(strings.TrimPrefix(mirror, "https://"), "/", "_")
return filepath.Join(cacheDir(), fmt.Sprintf("Sources_%s_%s_%s.xz", host, suite, component))
}
// cacheKey returns a unique key for mirror/suite/component combination
func (r *Resolver) cacheKey(mirror, suite, component string) string {
return fmt.Sprintf("%s:%s:%s", mirror, suite, component)
}
// loadCachedSources loads Sources.xz from cache or downloads it if not exists
func (r *Resolver) loadCachedSources(mirror, suite, component string) (*CachedSources, error) {
key := r.cacheKey(mirror, suite, component)
// Fast path: check if already loaded
r.sourcesMu.RLock()
if cached, exists := r.cachedSources[key]; exists {
r.sourcesMu.RUnlock()
return cached, nil
}
r.sourcesMu.RUnlock()
// Slow path: load with write lock
r.sourcesMu.Lock()
defer r.sourcesMu.Unlock()
// Double-check after acquiring write lock
if cached, exists := r.cachedSources[key]; exists {
return cached, nil
}
cachePath := r.cachePath(mirror, suite, component)
cached := &CachedSources{
packages: make(map[string]*sourceRecord),
path: cachePath,
}
// Try to load from disk cache first
if _, err := os.Stat(cachePath); err == nil {
// File exists on disk, parse it
if err := r.parseSourcesFileToCache(cachePath, mirror, cached); err == nil {
r.cachedSources[key] = cached
return cached, nil
}
}
// Download from HTTP
url := buildSourcesURL(mirror, suite, component, "xz")
if err := r.downloadSources(url, cachePath); err != nil {
// Try gz
url = buildSourcesURL(mirror, suite, component, "gz")
cachePath = strings.TrimSuffix(cachePath, ".xz") + ".gz"
if err := r.downloadSources(url, cachePath); err != nil {
// Try uncompressed
url = buildSourcesURL(mirror, suite, component, "")
cachePath = strings.TrimSuffix(cachePath, ".gz")
if err := r.downloadSources(url, cachePath); err != nil {
return nil, err
}
}
}
// Parse the downloaded file
if err := r.parseSourcesFileToCache(cachePath, mirror, cached); err != nil {
return nil, err
}
r.cachedSources[key] = cached
return cached, nil
}
// downloadSources downloads a Sources file from URL to local path
func (r *Resolver) downloadSources(url, localPath string) error {
fmt.Printf(" [resolver] Downloading %s...\n", url)
start := time.Now()
resp, err := r.client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("http %d", resp.StatusCode)
}
if err := ensureCacheDir(); err != nil {
return err
}
file, err := os.Create(localPath)
if err != nil {
return err
}
defer file.Close()
if _, err := io.Copy(file, resp.Body); err != nil {
os.Remove(localPath)
return err
}
fmt.Printf(" [resolver] Downloaded in %v\n", time.Since(start))
return nil
}
// parseSourcesFileToCache parses a local Sources file into specific CachedSources instance
func (r *Resolver) parseSourcesFileToCache(path, mirror string, cached *CachedSources) error {
fmt.Printf(" [resolver] Parsing %s...\n", filepath.Base(path))
start := time.Now()
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
var reader io.Reader = file
// Decompress if needed
if strings.HasSuffix(path, ".xz") {
r, err := xz.NewReader(file)
if err != nil {
return err
}
reader = r
} else if strings.HasSuffix(path, ".gz") {
r, err := gzip.NewReader(file)
if err != nil {
return err
}
defer r.Close()
reader = r
}
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 0, 64*1024), 8*1024*1024)
paragraph := make([]string, 0, 32)
count := 0
flush := func() {
if len(paragraph) == 0 {
return
}
rec, err := parseSourcesParagraph(paragraph)
paragraph = paragraph[:0]
if err != nil {
return
}
cached.mu.Lock()
cached.packages[rec.Package] = &rec
// Also index by binary names
for _, bin := range rec.Binaries {
bin = strings.TrimSpace(strings.ToLower(bin))
if bin != "" && bin != rec.Package {
cached.packages[bin] = &rec
}
}
cached.mu.Unlock()
count++
}
for scanner.Scan() {
line := scanner.Text()
if strings.TrimSpace(line) == "" {
flush()
continue
}
paragraph = append(paragraph, line)
}
flush()
if err := scanner.Err(); err != nil {
return err
}
cached.path = path
fmt.Printf(" [resolver] Parsed %d packages in %v\n", count, time.Since(start))
return nil
}
// lookupFromCache searches for a package in the in-memory cache
func (r *Resolver) lookupFromCache(pkg string, cached *CachedSources) (*sourceRecord, bool) {
cached.mu.RLock()
defer cached.mu.RUnlock()
if rec, ok := cached.packages[pkg]; ok {
return rec, true
}
return nil, false
}