nip/src/nimpak/npk_conversion.nim

514 lines
16 KiB
Nim

# 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