514 lines
16 KiB
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 |