nip/src/nimpak/adapters/pkgsrc.nim

571 lines
19 KiB
Nim

# nimpak/adapters/pkgsrc.nim
# PKGSRC grafting adapter for NetBSD package system
import std/[strutils, json, os, times, osproc, strformat]
import ../grafting
import ../types
type
PKGSRCAdapter* = ref object of PackageAdapter
pkgsrcPath*: string
binaryPackageUrl*: string
cacheDir*: string
useBinaryPackages*: bool
buildFromSource*: bool
makeFlags*: seq[string]
pkgDbPath*: string
PKGSRCPackageInfo* = object
name*: string
version*: string
category*: string
description*: string
homepage*: string
maintainer*: string
license*: string
depends*: seq[string]
conflicts*: seq[string]
pkgPath*: string
binaryUrl*: string
PKGSRCMakefile* = object
distname*: string
pkgname*: string
categories*: seq[string]
maintainer*: string
homepage*: string
comment*: string
license*: string
depends*: seq[string]
buildDepends*: seq[string]
conflicts*: seq[string]
# Forward declarations
proc findPKGSRCPackage(adapter: PKGSRCAdapter, packageName: string): PKGSRCPackageInfo
proc searchPKGSRCExact(adapter: PKGSRCAdapter, packageName: string): PKGSRCPackageInfo
proc searchPKGSRCFuzzy(adapter: PKGSRCAdapter, packageName: string): PKGSRCPackageInfo
proc searchPKGSRCOnline(adapter: PKGSRCAdapter, packageName: string): PKGSRCPackageInfo
proc parsePKGSRCMakefile(makefilePath: string, category: string, packageName: string): PKGSRCPackageInfo
proc getPKGSRCOnlineDetails(adapter: PKGSRCAdapter, category: string, packageName: string): PKGSRCPackageInfo
proc calculateFileHash(filePath: string): string
proc calculateDirectoryHash(dirPath: string): string
proc graftBinaryPackage(adapter: PKGSRCAdapter, info: PKGSRCPackageInfo, cache: GraftingCache): GraftResult
proc graftFromSource(adapter: PKGSRCAdapter, info: PKGSRCPackageInfo, cache: GraftingCache): GraftResult
proc newPKGSRCAdapter*(config: JsonNode = nil): PKGSRCAdapter =
## Create a new PKGSRC adapter with configuration
result = PKGSRCAdapter(
name: "pkgsrc",
priority: 25,
enabled: true,
pkgsrcPath: "/usr/pkgsrc",
binaryPackageUrl: "https://cdn.netbsd.org/pub/pkgsrc/packages/NetBSD",
cacheDir: "/var/cache/nip/pkgsrc",
useBinaryPackages: true,
buildFromSource: false,
makeFlags: @[],
pkgDbPath: "/var/db/pkg"
)
# Apply configuration if provided
if config != nil:
if config.hasKey("pkgsrc_path"):
result.pkgsrcPath = config["pkgsrc_path"].getStr()
if config.hasKey("binary_package_url"):
result.binaryPackageUrl = config["binary_package_url"].getStr()
if config.hasKey("cache_dir"):
result.cacheDir = config["cache_dir"].getStr()
if config.hasKey("use_binary_packages"):
result.useBinaryPackages = config["use_binary_packages"].getBool()
if config.hasKey("build_from_source"):
result.buildFromSource = config["build_from_source"].getBool()
if config.hasKey("make_flags"):
result.makeFlags = @[]
for flag in config["make_flags"]:
result.makeFlags.add(flag.getStr())
method graftPackage*(adapter: PKGSRCAdapter, packageName: string, cache: GraftingCache): GraftResult =
## Graft a package from PKGSRC
echo fmt"🌱 Grafting package from PKGSRC: {packageName}"
var result = GraftResult(
success: false,
packageId: packageName,
errors: @[]
)
try:
# First, find the package in PKGSRC
let packageInfo = findPKGSRCPackage(adapter, packageName)
if packageInfo.name == "":
result.errors.add(fmt"Package '{packageName}' not found in PKGSRC")
return result
# Try binary package first if enabled
if adapter.useBinaryPackages:
echo "🔍 Trying binary package..."
let binaryResult = graftBinaryPackage(adapter, packageInfo, cache)
if binaryResult.success:
return binaryResult
# Fall back to building from source if enabled
if adapter.buildFromSource:
echo "🔨 Building from source..."
let sourceResult = graftFromSource(adapter, packageInfo, cache)
if sourceResult.success:
return sourceResult
result.errors.add("Neither binary package nor source build succeeded")
except Exception as e:
result.errors.add(fmt"Exception during PKGSRC grafting: {e.msg}")
result
proc findPKGSRCPackage(adapter: PKGSRCAdapter, packageName: string): PKGSRCPackageInfo =
## Find a package in the PKGSRC tree
var info = PKGSRCPackageInfo()
try:
# First try to find by exact name
let exactResult = searchPKGSRCExact(adapter, packageName)
if exactResult.name != "":
return exactResult
# Try fuzzy search
let fuzzyResult = searchPKGSRCFuzzy(adapter, packageName)
if fuzzyResult.name != "":
return fuzzyResult
# Try online package database
let onlineResult = searchPKGSRCOnline(adapter, packageName)
if onlineResult.name != "":
return onlineResult
except Exception as e:
echo fmt"Warning: Error searching PKGSRC: {e.msg}"
info
proc searchPKGSRCExact(adapter: PKGSRCAdapter, packageName: string): PKGSRCPackageInfo =
## Search for exact package name in local PKGSRC tree
var info = PKGSRCPackageInfo()
try:
if not dirExists(adapter.pkgsrcPath):
return info
# Search through categories
for category in walkDirs(adapter.pkgsrcPath / "*"):
let categoryName = extractFilename(category)
if categoryName in ["CVS", "distfiles", "packages", "bootstrap"]:
continue
let packageDir = category / packageName
if dirExists(packageDir):
let makefilePath = packageDir / "Makefile"
if fileExists(makefilePath):
info = parsePKGSRCMakefile(makefilePath, categoryName, packageName)
if info.name != "":
return info
except Exception as e:
echo fmt"Warning: Error in exact PKGSRC search: {e.msg}"
info
proc searchPKGSRCFuzzy(adapter: PKGSRCAdapter, packageName: string): PKGSRCPackageInfo =
## Fuzzy search for package name in PKGSRC
var info = PKGSRCPackageInfo()
try:
if not dirExists(adapter.pkgsrcPath):
return info
# Use find command for fuzzy search
let findCmd = fmt"find {adapter.pkgsrcPath} -name '*{packageName}*' -type d -maxdepth 2"
let (output, exitCode) = execCmdEx(findCmd)
if exitCode == 0:
for line in output.splitLines():
if line.len > 0 and line.contains("/"):
let parts = line.split("/")
if parts.len >= 2:
let category = parts[^2]
let pkgName = parts[^1]
let makefilePath = line / "Makefile"
if fileExists(makefilePath):
info = parsePKGSRCMakefile(makefilePath, category, pkgName)
if info.name != "":
return info
except Exception as e:
echo fmt"Warning: Error in fuzzy PKGSRC search: {e.msg}"
info
proc searchPKGSRCOnline(adapter: PKGSRCAdapter, packageName: string): PKGSRCPackageInfo =
## Search for package in online PKGSRC database
var info = PKGSRCPackageInfo()
try:
# Query the NetBSD package database
let searchUrl = fmt"https://pkgsrc.se/search?q={packageName}"
let curlCmd = "curl -s '" & searchUrl & "' | grep -o 'href=\"/[^/]*/[^\"]*\"' | head -1"
let (output, exitCode) = execCmdEx(curlCmd)
if exitCode == 0 and output.len > 0:
# Parse the result to extract category and package name
let href = output.strip()
if href.startsWith("href=\"/") and href.endsWith("\""):
let path = href[7..^2] # Remove href="/ and "
let parts = path.split("/")
if parts.len == 2:
info.category = parts[0]
info.name = parts[1]
info.pkgPath = fmt"{info.category}/{info.name}"
# Try to get more details
let detailsResult = getPKGSRCOnlineDetails(adapter, info.category, info.name)
if detailsResult.description != "":
info = detailsResult
except Exception as e:
echo fmt"Warning: Error in online PKGSRC search: {e.msg}"
info
proc getPKGSRCOnlineDetails(adapter: PKGSRCAdapter, category: string, packageName: string): PKGSRCPackageInfo =
## Get detailed package information from online PKGSRC database
var info = PKGSRCPackageInfo(
name: packageName,
category: category,
pkgPath: fmt"{category}/{packageName}"
)
try:
let detailUrl = fmt"https://pkgsrc.se/{category}/{packageName}"
let curlCmd = fmt"curl -s '{detailUrl}'"
let (output, exitCode) = execCmdEx(curlCmd)
if exitCode == 0:
# Parse HTML to extract package information
for line in output.splitLines():
if "Description:" in line:
# Extract description from HTML
let start = line.find(">") + 1
let endTag = line.find("</", start)
if start > 0 and endTag > start:
info.description = line[start..<endTag].strip()
elif "Homepage:" in line and "href=" in line:
# Extract homepage URL
let hrefStart = line.find("href=\"") + 6
let hrefEnd = line.find("\"", hrefStart)
if hrefStart > 5 and hrefEnd > hrefStart:
info.homepage = line[hrefStart..<hrefEnd]
except Exception as e:
echo fmt"Warning: Error getting PKGSRC online details: {e.msg}"
info
proc parsePKGSRCMakefile(makefilePath: string, category: string, packageName: string): PKGSRCPackageInfo =
## Parse a PKGSRC Makefile to extract package information
var info = PKGSRCPackageInfo(
name: packageName,
category: category,
pkgPath: fmt"{category}/{packageName}"
)
try:
let content = readFile(makefilePath)
for line in content.splitLines():
let trimmed = line.strip()
if trimmed.startsWith("#") or trimmed.len == 0:
continue
if trimmed.startsWith("DISTNAME="):
let distname = trimmed[9..^1].strip()
# Extract version from distname
let parts = distname.split("-")
if parts.len > 1:
info.version = parts[^1]
elif trimmed.startsWith("PKGNAME="):
let pkgname = trimmed[8..^1].strip()
if "-" in pkgname:
let parts = pkgname.split("-")
if parts.len > 1:
info.version = parts[^1]
elif trimmed.startsWith("COMMENT="):
info.description = trimmed[8..^1].strip()
elif trimmed.startsWith("HOMEPAGE="):
info.homepage = trimmed[9..^1].strip()
elif trimmed.startsWith("MAINTAINER="):
info.maintainer = trimmed[11..^1].strip()
elif trimmed.startsWith("LICENSE="):
info.license = trimmed[8..^1].strip()
elif trimmed.startsWith("DEPENDS+="):
let dep = trimmed[9..^1].strip()
info.depends.add(dep)
elif trimmed.startsWith("CONFLICTS+="):
let conflict = trimmed[11..^1].strip()
info.conflicts.add(conflict)
except Exception as e:
echo fmt"Warning: Error parsing PKGSRC Makefile: {e.msg}"
info
proc graftBinaryPackage(adapter: PKGSRCAdapter, info: PKGSRCPackageInfo, cache: GraftingCache): GraftResult =
## Graft a binary package from PKGSRC
var result = GraftResult(success: false, errors: @[])
try:
# Construct binary package URL
let arch = "x86_64" # TODO: Detect actual architecture
let osVersion = "9.0" # TODO: Detect NetBSD version or use generic
let binaryUrl = fmt"{adapter.binaryPackageUrl}/{arch}/{osVersion}/All/{info.name}-{info.version}.tgz"
echo fmt"📦 Downloading binary package: {binaryUrl}"
# Download binary package
let packageFile = adapter.cacheDir / fmt"{info.name}-{info.version}.tgz"
createDir(adapter.cacheDir)
let downloadCmd = fmt"curl -L -o {packageFile} {binaryUrl}"
let (downloadOutput, downloadExit) = execCmdEx(downloadCmd)
if downloadExit != 0:
result.errors.add(fmt"Failed to download binary package: {downloadOutput}")
return result
if not fileExists(packageFile):
result.errors.add("Binary package file not found after download")
return result
# Extract binary package
let extractDir = adapter.cacheDir / "extracted" / info.name
if dirExists(extractDir):
removeDir(extractDir)
createDir(extractDir)
let extractCmd = fmt"tar -xzf {packageFile} -C {extractDir}"
let (extractOutput, extractExit) = execCmdEx(extractCmd)
if extractExit != 0:
result.errors.add(fmt"Failed to extract binary package: {extractOutput}")
return result
# Calculate hashes
let originalHash = calculateFileHash(packageFile)
let graftHash = calculateGraftHash(info.name, "pkgsrc", now())
# Create metadata
let metadata = GraftedPackageMetadata(
packageName: info.name,
version: info.version,
source: "pkgsrc-binary",
graftedAt: now(),
originalHash: originalHash,
graftHash: graftHash,
buildLog: fmt"Downloaded binary package from {binaryUrl}",
provenance: ProvenanceInfo(
originalSource: "pkgsrc-binary",
downloadUrl: binaryUrl,
archivePath: packageFile,
extractedPath: extractDir,
conversionLog: fmt"Extracted PKGSRC binary package to {extractDir}"
)
)
result.success = true
result.packageId = info.name
result.metadata = metadata
echo fmt"✅ Successfully grafted PKGSRC binary package: {info.name} {info.version}"
except Exception as e:
result.errors.add(fmt"Exception in binary package grafting: {e.msg}")
result
proc graftFromSource(adapter: PKGSRCAdapter, info: PKGSRCPackageInfo, cache: GraftingCache): GraftResult =
## Build and graft a package from PKGSRC source
var result = GraftResult(success: false, errors: @[])
try:
if not dirExists(adapter.pkgsrcPath):
result.errors.add(fmt"PKGSRC tree not found at {adapter.pkgsrcPath}")
return result
let packageDir = adapter.pkgsrcPath / info.pkgPath
if not dirExists(packageDir):
result.errors.add(fmt"Package directory not found: {packageDir}")
return result
echo fmt"🔨 Building PKGSRC package from source: {packageDir}"
# Build the package using bmake
var buildCmd = fmt"cd {packageDir} && bmake"
for flag in adapter.makeFlags:
buildCmd.add(fmt" {flag}")
let (buildOutput, buildExit) = execCmdEx(buildCmd)
if buildExit != 0:
result.errors.add(fmt"PKGSRC build failed: {buildOutput}")
return result
# Install to temporary directory
let installDir = adapter.cacheDir / "built" / info.name
if dirExists(installDir):
removeDir(installDir)
createDir(installDir)
let installCmd = fmt"cd {packageDir} && bmake DESTDIR={installDir} install"
let (installOutput, installExit) = execCmdEx(installCmd)
if installExit != 0:
result.errors.add(fmt"PKGSRC install failed: {installOutput}")
return result
# Calculate hashes
let sourceHash = calculateDirectoryHash(packageDir)
let graftHash = calculateGraftHash(info.name, "pkgsrc", now())
# Create metadata
let metadata = GraftedPackageMetadata(
packageName: info.name,
version: info.version,
source: "pkgsrc-source",
graftedAt: now(),
originalHash: sourceHash,
graftHash: graftHash,
buildLog: buildOutput & "\n" & installOutput,
provenance: ProvenanceInfo(
originalSource: "pkgsrc-source",
downloadUrl: fmt"https://github.com/NetBSD/pkgsrc/tree/trunk/{info.pkgPath}",
archivePath: packageDir,
extractedPath: installDir,
conversionLog: fmt"Built from PKGSRC source and installed to {installDir}"
)
)
result.success = true
result.packageId = info.name
result.metadata = metadata
echo fmt"✅ Successfully built PKGSRC package from source: {info.name} {info.version}"
except Exception as e:
result.errors.add(fmt"Exception in source build: {e.msg}")
result
proc calculateFileHash(filePath: string): string =
## Calculate hash of a file
try:
let hashCmd = fmt"sha256sum {filePath}"
let (output, exitCode) = execCmdEx(hashCmd)
if exitCode == 0:
return "pkgsrc-" & output.split()[0]
except:
discard
"pkgsrc-hash-error"
proc calculateDirectoryHash(dirPath: string): string =
## Calculate hash of directory contents
try:
let hashCmd = fmt"find {dirPath} -type f -exec sha256sum {{}} + | sha256sum"
let (output, exitCode) = execCmdEx(hashCmd)
if exitCode == 0:
return "pkgsrc-src-" & output.split()[0]
except:
discard
"pkgsrc-src-hash-error"
method validatePackage*(adapter: PKGSRCAdapter, packageName: string): Result[bool, string] =
## Validate that a package exists in PKGSRC
try:
let info = findPKGSRCPackage(adapter, packageName)
return Result[bool, string](isOk: true, okValue: info.name != "")
except Exception as e:
return Result[bool, string](isOk: false, errValue: fmt"Validation error: {e.msg}")
method getPackageInfo*(adapter: PKGSRCAdapter, packageName: string): Result[JsonNode, string] =
## Get detailed package information from PKGSRC
try:
let info = findPKGSRCPackage(adapter, packageName)
if info.name == "":
return Result[JsonNode, string](isOk: false, errValue: fmt"Package '{packageName}' not found in PKGSRC")
let result = %*{
"name": info.name,
"version": info.version,
"category": info.category,
"description": info.description,
"homepage": info.homepage,
"maintainer": info.maintainer,
"license": info.license,
"depends": info.depends,
"conflicts": info.conflicts,
"pkg_path": info.pkgPath,
"source": "pkgsrc",
"adapter": adapter.name
}
return Result[JsonNode, string](isOk: true, okValue: result)
except Exception as e:
return Result[JsonNode, string](isOk: false, errValue: fmt"Error getting package info: {e.msg}")
# Utility functions
proc isPKGSRCAvailable*(adapter: PKGSRCAdapter): bool =
## Check if PKGSRC is available on the system
dirExists(adapter.pkgsrcPath) or findExe("bmake") != ""
proc commandExists(command: string): bool =
## Check if a command exists in PATH
try:
let (_, exitCode) = execCmdEx(fmt"which {command}")
return exitCode == 0
except:
return false
proc listPKGSRCCategories*(adapter: PKGSRCAdapter): seq[string] =
## List available PKGSRC categories
result = @[]
try:
if dirExists(adapter.pkgsrcPath):
for category in walkDirs(adapter.pkgsrcPath / "*"):
let categoryName = extractFilename(category)
if categoryName notin ["CVS", "distfiles", "packages", "bootstrap"]:
result.add(categoryName)
except:
discard
proc listPKGSRCPackages*(adapter: PKGSRCAdapter, category: string = ""): seq[string] =
## List packages in PKGSRC (optionally filtered by category)
result = @[]
try:
if not dirExists(adapter.pkgsrcPath):
return result
if category != "":
let categoryDir = adapter.pkgsrcPath / category
if dirExists(categoryDir):
for pkg in walkDirs(categoryDir / "*"):
let pkgName = extractFilename(pkg)
if pkgName != "CVS":
result.add(pkgName)
else:
for cat in listPKGSRCCategories(adapter):
for pkg in listPKGSRCPackages(adapter, cat):
result.add(fmt"{cat}/{pkg}")
except:
discard