# SPDX-License-Identifier: LSL-1.0 # Copyright (c) 2026 Markus Maiwald # Stewardship: Self Sovereign Society Foundation # # This file is part of the Nexus Sovereign Core. # See legal/LICENSE_SOVEREIGN.md for license terms. # nimpak/npk_conversion.nim # Enhanced NPK conversion with build hash integration import std/[strutils, json, os, times, tables, sequtils, strformat, algorithm, osproc] import ./types import utils/resultutils import types/grafting_types type NPKConverter* = object outputDir*: string compressionLevel*: int includeProvenance*: bool calculateBuildHash*: bool signPackages*: bool keyPath*: string BuildConfiguration* = object sourceHash*: string sourceTimestamp*: DateTime configureFlags*: seq[string] compilerFlags*: seq[string] linkerFlags*: seq[string] compilerVersion*: string nimVersion*: string nimFlags*: seq[string] targetArchitecture*: string libc*: string libcVersion*: string allocator*: string allocatorVersion*: string environmentVars*: Table[string, string] dependencies*: seq[DependencyHash] DependencyHash* = object packageName*: string buildHash*: string BuildHash* = object hash*: string algorithm*: string components*: seq[string] timestamp*: DateTime NPKManifest* = object name*: string version*: string description*: string homepage*: string license*: seq[string] maintainer*: string buildHash*: string sourceHash*: string artifactHash*: string buildConfig*: BuildConfiguration dependencies*: seq[DependencyHash] acul*: AculCompliance files*: seq[NPKFile] provenance*: ProvenanceInfo created*: DateTime converterName*: string NPKFile* = object path*: string hash*: string permissions*: string size*: int64 ConversionResult* = object success*: bool npkPath*: string manifest*: NPKManifest buildHash*: BuildHash errors*: seq[string] # Hash-relevant environment variables (from SHARED_SPECIFICATIONS.md) const HASH_RELEVANT_ENV_VARS* = [ "CC", "CXX", "CFLAGS", "CXXFLAGS", "LDFLAGS", "PKG_CONFIG_PATH", "PATH", "LD_LIBRARY_PATH", "MAKEFLAGS", "DESTDIR" ] proc newNPKConverter*(outputDir: string = ""): NPKConverter = ## Create a new NPK converter NPKConverter( outputDir: if outputDir == "": getTempDir() / "nimpak-npk" else: outputDir, compressionLevel: 6, includeProvenance: true, calculateBuildHash: true, signPackages: false, keyPath: "" ) proc convertToNPK*(conv: NPKConverter, metadata: GraftedPackageMetadata, extractedPath: string): Result[ConversionResult, string] = ## Convert a grafted package to NPK format with build hash integration echo fmt"🔄 Converting {metadata.packageName} to NPK format..." var result = ConversionResult(success: false, errors: @[]) try: # Create output directory if not dirExists(conv.outputDir): createDir(conv.outputDir) # Generate NPK manifest let manifestResult = generateNPKManifest(conv, metadata, extractedPath) if manifestResult.isErr: result.errors.add("Failed to generate manifest: " & manifestResult.error) return ok(result) let manifest = manifestResult.get() result.manifest = manifest # Calculate build hash if enabled if conv.calculateBuildHash: let buildHashResult = calculateBuildHash(manifest.buildConfig) if buildHashResult.isErr: result.errors.add("Failed to calculate build hash: " & buildHashResult.error) return ok(result) result.buildHash = buildHashResult.get() result.manifest.buildHash = result.buildHash.hash # Create NPK package let npkResult = createNPKPackage(conv, manifest, extractedPath) if npkResult.isErr: result.errors.add("Failed to create NPK package: " & npkResult.error) return ok(result) result.npkPath = npkResult.get() result.success = true echo fmt"✅ Successfully converted to NPK: {result.npkPath}" except Exception as e: result.errors.add(fmt"Exception during conversion: {e.msg}") ok(result) proc generateNPKManifest(conv: NPKConverter, metadata: GraftedPackageMetadata, extractedPath: string): Result[NPKManifest, string] = ## Generate NPK manifest from grafted package metadata try: # Scan files in extracted path let filesResult = scanPackageFiles(extractedPath) if filesResult.isErr: return err("Failed to scan package files: " & filesResult.error) let files = filesResult.get() # Extract build configuration from metadata let buildConfig = extractBuildConfiguration(metadata) # Calculate artifact hash let artifactHash = calculateArtifactHash(files) let manifest = NPKManifest( name: metadata.packageName, version: metadata.version, description: extractDescription(metadata), homepage: extractHomepage(metadata), license: extractLicense(metadata), maintainer: extractMaintainer(metadata), buildHash: "", # Will be filled later sourceHash: metadata.originalHash, artifactHash: artifactHash, buildConfig: buildConfig, dependencies: extractDependencies(metadata), acul: AculCompliance( required: false, membership: "NexusOS-Community", attribution: fmt"Grafted from {metadata.source}", buildLog: metadata.buildLog ), files: files, provenance: metadata.provenance, created: now(), converterName: "nimpak-" & metadata.source ) ok(manifest) except Exception as e: err(fmt"Exception generating manifest: {e.msg}") proc scanPackageFiles(extractedPath: string): Result[seq[NPKFile], string] = ## Scan extracted package directory and create file manifest var files: seq[NPKFile] = @[] try: if not dirExists(extractedPath): return err(fmt"Extracted path does not exist: {extractedPath}") # Walk through all files for file in walkDirRec(extractedPath): let relativePath = file.replace(extractedPath, "").replace("\\", "/") if relativePath.startsWith("/"): let cleanPath = relativePath[1..^1] else: let cleanPath = relativePath if fileExists(file): let info = getFileInfo(file) let hash = calculateFileHash(file) files.add(NPKFile( path: "/" & cleanPath, hash: hash, permissions: getFilePermissions(file), size: info.size )) ok(files) except Exception as e: err(fmt"Exception scanning files: {e.msg}") proc extractBuildConfiguration(metadata: GraftedPackageMetadata): BuildConfiguration = ## Extract build configuration from grafted package metadata BuildConfiguration( sourceHash: metadata.originalHash, sourceTimestamp: metadata.graftedAt, configureFlags: extractConfigureFlags(metadata), compilerFlags: extractCompilerFlags(metadata), linkerFlags: @[], compilerVersion: extractCompilerVersion(metadata), nimVersion: "2.0.0", # TODO: Get actual Nim version nimFlags: @[], targetArchitecture: "x86_64", # TODO: Detect actual architecture libc: detectLibc(metadata), libcVersion: detectLibcVersion(metadata), allocator: "default", allocatorVersion: "system", environmentVars: extractEnvironmentVars(metadata), dependencies: extractDependencies(metadata) ) proc calculateBuildHash*(config: BuildConfiguration): Result[BuildHash, string] = ## Calculate build hash using the shared algorithm from SHARED_SPECIFICATIONS.md try: var components: seq[string] = @[] # 1. Source integrity (sorted deterministically) components.add(config.sourceHash) components.add(config.sourceTimestamp.format("yyyy-MM-dd'T'HH:mm:ss'Z'")) # 2. Build configuration (sorted alphabetically) components.add(config.configureFlags.sorted().join(" ")) components.add(config.compilerFlags.sorted().join(" ")) components.add(config.linkerFlags.sorted().join(" ")) # 3. Toolchain fingerprint components.add(config.compilerVersion) components.add(config.nimVersion & " " & config.nimFlags.sorted().join(" ")) # 4. Target environment components.add(config.targetArchitecture) components.add(config.libc & "-" & config.libcVersion) components.add(config.allocator & "-" & config.allocatorVersion) # 5. Environment variables (filtered and sorted) let envVars = config.environmentVars.keys.toSeq.sorted() for key in envVars: if key in HASH_RELEVANT_ENV_VARS: components.add(key & "=" & config.environmentVars[key]) # 6. Dependency hashes (sorted by package name) let sortedDeps = config.dependencies.sortedByIt(it.packageName) for dep in sortedDeps: components.add(dep.packageName & ":" & dep.buildHash) # Calculate final hash let input = components.join("|") let hash = blake3Hash(input) ok(BuildHash( hash: hash, algorithm: "blake3", components: components, timestamp: now() )) except Exception as e: err(fmt"Exception calculating build hash: {e.msg}") proc blake3Hash(input: string): string = ## Calculate BLAKE3 hash (placeholder implementation) # TODO: Use actual BLAKE3 when available "blake3-" & $hash(input) proc createNPKPackage(conv: NPKConverter, manifest: NPKManifest, extractedPath: string): Result[string, string] = ## Create the actual NPK package file try: let npkPath = conv.outputDir / fmt"{manifest.name}-{manifest.version}.npk" # Create manifest file let manifestPath = conv.outputDir / "manifest.kdl" let manifestResult = writeManifestKDL(manifest, manifestPath) if manifestResult.isErr: return err("Failed to write manifest: " & manifestResult.error) # Create files archive let filesArchivePath = conv.outputDir / "files.tar.zst" let archiveResult = createFilesArchive(extractedPath, filesArchivePath, conv.compressionLevel) if archiveResult.isErr: return err("Failed to create files archive: " & archiveResult.error) # Create build log file let buildLogPath = conv.outputDir / "build.log" writeFile(buildLogPath, manifest.provenance.conversionLog) # Create final NPK package let createCmd = fmt"tar -czf {npkPath} -C {conv.outputDir} manifest.kdl files.tar.zst build.log" let (output, exitCode) = execCmdEx(createCmd) if exitCode != 0: return err(fmt"Failed to create NPK package: {output}") # Sign package if enabled if conv.signPackages and conv.keyPath != "": let signResult = signNPKPackage(npkPath, conv.keyPath) if signResult.isErr: echo fmt"⚠️ Warning: Failed to sign package: {signResult.error}" ok(npkPath) except Exception as e: err(fmt"Exception creating NPK package: {e.msg}") proc writeManifestKDL(manifest: NPKManifest, outputPath: string): Result[void, string] = ## Write NPK manifest in KDL format try: var kdl = fmt"""// NPK Package Manifest package "{manifest.name}" {{ version "{manifest.version}" description "{manifest.description}" // Build configuration and hashes build_hash "{manifest.buildHash}" source_hash "{manifest.sourceHash}" artifact_hash "{manifest.artifactHash}" build_config {{ configure_flags {formatStringArray(manifest.buildConfig.configureFlags)} compiler_flags {formatStringArray(manifest.buildConfig.compilerFlags)} target_architecture "{manifest.buildConfig.targetArchitecture}" libc "{manifest.buildConfig.libc}-{manifest.buildConfig.libcVersion}" allocator "{manifest.buildConfig.allocator}-{manifest.buildConfig.allocatorVersion}" }} // Dependencies with their build hashes dependencies {{ """ for dep in manifest.dependencies: kdl.add(fmt""" {dep.packageName} {{ build_hash "{dep.buildHash}" }} """) kdl.add(fmt""" }} // ACUL compliance metadata acul {{ required {manifest.acul.required} membership "{manifest.acul.membership}" license "{manifest.license.join(", ")}" attribution "{manifest.acul.attribution}" }} // File manifest files {{ """) for file in manifest.files: kdl.add(fmt""" "{file.path}" {{ hash "{file.hash}" permissions "{file.permissions}" size {file.size} }} """) kdl.add(fmt""" }} // Provenance information provenance {{ original_source "{manifest.provenance.originalSource}" download_url "{manifest.provenance.downloadUrl}" converted_at "{manifest.created}" converter "{manifest.converterName}" }} }} """) writeFile(outputPath, kdl) ok() except Exception as e: err(fmt"Exception writing manifest: {e.msg}") # Helper functions for metadata extraction proc extractDescription(metadata: GraftedPackageMetadata): string = # TODO: Extract from build log or metadata fmt"Package {metadata.packageName} grafted from {metadata.source}" proc extractHomepage(metadata: GraftedPackageMetadata): string = # TODO: Extract from package metadata "" proc extractLicense(metadata: GraftedPackageMetadata): seq[string] = # TODO: Extract from package metadata @["unknown"] proc extractMaintainer(metadata: GraftedPackageMetadata): string = # TODO: Extract from package metadata "NimPak Grafting System" proc extractConfigureFlags(metadata: GraftedPackageMetadata): seq[string] = # TODO: Parse from build log @[] proc extractCompilerFlags(metadata: GraftedPackageMetadata): seq[string] = # TODO: Parse from build log @["-O2"] proc extractCompilerVersion(metadata: GraftedPackageMetadata): string = # TODO: Detect from system "gcc-11.0" proc detectLibc(metadata: GraftedPackageMetadata): string = # TODO: Detect actual libc "musl" proc detectLibcVersion(metadata: GraftedPackageMetadata): string = # TODO: Detect actual version "1.2.4" proc extractEnvironmentVars(metadata: GraftedPackageMetadata): Table[string, string] = # TODO: Extract from build environment initTable[string, string]() proc extractDependencies(metadata: GraftedPackageMetadata): seq[DependencyHash] = # TODO: Extract from package metadata @[] proc calculateArtifactHash(files: seq[NPKFile]): string = ## Calculate hash of all package files var input = "" for file in files.sortedByIt(it.path): input.add(file.path & ":" & file.hash & ":" & $file.size & "|") "artifact-" & $hash(input) proc calculateFileHash(filePath: string): string = ## Calculate hash of individual file try: # TODO: Use actual BLAKE3 when available let content = readFile(filePath) "file-" & $hash(content) except: "file-hash-error" proc getFilePermissions(filePath: string): string = ## Get file permissions as string try: let info = getFileInfo(filePath) # TODO: Convert FilePermissions to octal string "644" # Default for now except: "644" proc createFilesArchive(sourcePath: string, archivePath: string, compressionLevel: int): Result[void, string] = ## Create compressed archive of package files try: let cmd = fmt"tar -cf - -C {sourcePath} . | zstd -{compressionLevel} -o {archivePath}" let (output, exitCode) = execCmdEx(cmd) if exitCode != 0: return err(fmt"Archive creation failed: {output}") ok() except Exception as e: err(fmt"Exception creating archive: {e.msg}") proc signNPKPackage(npkPath: string, keyPath: string): Result[void, string] = ## Sign NPK package with cryptographic signature try: # TODO: Implement actual signing with Ed25519 let signaturePath = npkPath & ".sig" writeFile(signaturePath, "placeholder-signature") ok() except Exception as e: err(fmt"Exception signing package: {e.msg}") proc formatStringArray(arr: seq[string]): string = ## Format string array for KDL output if arr.len == 0: return "\"\"" var result = "" for i, item in arr: if i > 0: result.add(" ") result.add(fmt"\"{item}\"") result