pisechka
This commit is contained in:
parent
98adef8fe7
commit
f07f62244c
3 changed files with 313 additions and 62 deletions
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,7 +40,15 @@ 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 {
|
||||||
|
|
@ -45,6 +56,10 @@ type Resolver struct {
|
||||||
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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue