571 lines
19 KiB
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 |