# 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(" 0 and endTag > start: info.description = line[start.. 5 and hrefEnd > hrefStart: info.homepage = line[hrefStart.. 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