# 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 Migration Tools ## ## Tools for migrating from legacy formats and other package managers. ## Task 42: Implement migration tools. import std/[os, strutils, strformat, json, tables, sequtils, times] import ./types import cas import logging type MigrationSource* = enum OldNip, ## Legacy NIP format Flatpak, ## Flatpak applications Snap, ## Snap packages AppImage, ## AppImage files Docker, ## Docker images Nix, ## Nix packages Pkgsrc ## pkgsrc packages MigrationResult* = object success*: bool source*: MigrationSource packageName*: string targetPath*: string casHashes*: seq[string] errors*: seq[string] warnings*: seq[string] MigrationStats* = object totalFiles*: int totalBytes*: int64 dedupBytes*: int64 casObjects*: int startTime*: DateTime endTime*: DateTime MigrationManager* = object casManager*: CasManager logger*: Logger dryRun*: bool verbose*: bool # ############################################################################ # Migration Manager Initialization # ############################################################################ proc initMigrationManager*(casRoot: string, dryRun: bool = false, verbose: bool = false): MigrationManager = result = MigrationManager( casManager: initCasManager(casRoot, casRoot / "system"), logger: initLogger("migration", if verbose: Debug else: Info, {Console}), dryRun: dryRun, verbose: verbose ) # ############################################################################ # Legacy NIP Migration # ############################################################################ proc migrateLegacyNip*(mm: var MigrationManager, legacyPath: string): MigrationResult = ## Migrate from legacy NIP format (pre-unified storage) result = MigrationResult( success: false, source: OldNip, packageName: legacyPath.extractFilename, casHashes: @[], errors: @[], warnings: @[] ) mm.logger.log(Info, fmt"Migrating legacy NIP: {legacyPath}") if not dirExists(legacyPath): result.errors.add(fmt"Legacy NIP directory not found: {legacyPath}") return # Check for legacy manifest let manifestPath = legacyPath / "manifest.kdl" let legacyManifest = legacyPath / "package.json" if not fileExists(manifestPath) and not fileExists(legacyManifest): result.errors.add("No manifest found (manifest.kdl or package.json)") return # Enumerate files to migrate var stats = MigrationStats(startTime: now()) for file in walkDirRec(legacyPath): if file.endswith(".nip-old") or file.contains("/.git/"): continue stats.totalFiles += 1 let fileInfo = getFileInfo(file) stats.totalBytes += fileInfo.size if not mm.dryRun: # Store each file in CAS let storeResult = mm.casManager.storeFile(file) if storeResult.isOk: let obj = storeResult.get() result.casHashes.add(obj.hash) stats.casObjects += 1 stats.dedupBytes += obj.size - obj.compressedSize mm.logger.log(Debug, fmt"Stored: {file} -> {obj.hash[0..15]}...") else: result.warnings.add(fmt"Failed to store: {file}") stats.endTime = now() # Generate migration report mm.logger.log(Info, fmt"Migration stats: {stats.totalFiles} files, {stats.totalBytes} bytes") mm.logger.log(Info, fmt"CAS objects: {stats.casObjects}, Dedup savings: {stats.dedupBytes} bytes") result.success = result.errors.len == 0 result.targetPath = mm.casManager.rootPath / "migrated" / result.packageName # ############################################################################ # Flatpak Migration # ############################################################################ proc migrateFlatpak*(mm: var MigrationManager, appId: string): MigrationResult = ## Migrate a Flatpak application to NIP format result = MigrationResult( success: false, source: Flatpak, packageName: appId, casHashes: @[], errors: @[], warnings: @[] ) mm.logger.log(Info, fmt"Migrating Flatpak app: {appId}") # Check if flatpak is installed let flatpakPath = "/var/lib/flatpak/app" / appId let userFlatpakPath = getHomeDir() / ".local/share/flatpak/app" / appId var sourcePath = "" if dirExists(flatpakPath): sourcePath = flatpakPath elif dirExists(userFlatpakPath): sourcePath = userFlatpakPath else: result.errors.add(fmt"Flatpak app not found: {appId}") result.warnings.add("Ensure the app is installed via 'flatpak install'") return # Find current version var currentDir = "" for dir in walkDir(sourcePath / "current"): if dir.kind == pcDir or dir.kind == pcLinkToDir: currentDir = dir.path break if currentDir == "": result.errors.add("Could not find current version") return # Migrate files let filesDir = currentDir / "files" if dirExists(filesDir): for file in walkDirRec(filesDir): if not mm.dryRun: let storeResult = mm.casManager.storeFile(file) if storeResult.isOk: result.casHashes.add(storeResult.get().hash) # Create NIP manifest from Flatpak metadata let metadataPath = currentDir / "metadata" if fileExists(metadataPath): mm.logger.log(Debug, "Found Flatpak metadata, will convert to NIP manifest") result.success = result.errors.len == 0 result.targetPath = getHomeDir() / ".local/share/nexus/nips" / appId # ############################################################################ # AppImage Migration # ############################################################################ proc migrateAppImage*(mm: var MigrationManager, appImagePath: string): MigrationResult = ## Migrate an AppImage to NIP format result = MigrationResult( success: false, source: AppImage, packageName: appImagePath.extractFilename.changeFileExt(""), casHashes: @[], errors: @[], warnings: @[] ) mm.logger.log(Info, fmt"Migrating AppImage: {appImagePath}") if not fileExists(appImagePath): result.errors.add(fmt"AppImage not found: {appImagePath}") return # AppImages are squashfs, need to extract result.warnings.add("AppImage extraction requires 'unsquashfs' - placeholder") # Store the AppImage itself as a blob for now if not mm.dryRun: let storeResult = mm.casManager.storeFile(appImagePath) if storeResult.isOk: result.casHashes.add(storeResult.get().hash) result.success = true # ############################################################################ # Docker/OCI Migration # ############################################################################ proc migrateDockerImage*(mm: var MigrationManager, imageName: string): MigrationResult = ## Migrate a Docker image to NEXTER format result = MigrationResult( success: false, source: Docker, packageName: imageName.replace(":", "-").replace("/", "-"), casHashes: @[], errors: @[], warnings: @[] ) mm.logger.log(Info, fmt"Migrating Docker image: {imageName}") # Export Docker image to tar let exportPath = getTempDir() / fmt"docker-export-{result.packageName}.tar" result.warnings.add("Docker migration requires 'docker save' - placeholder") result.warnings.add(fmt"Would export to: {exportPath}") # TODO: Actually run docker save and process layers # Each Docker layer can be stored as a CAS chunk for deduplication result.success = true # Placeholder # ############################################################################ # Nix Package Migration # ############################################################################ proc migrateNixPackage*(mm: var MigrationManager, nixStorePath: string): MigrationResult = ## Migrate a Nix store path to NPK format result = MigrationResult( success: false, source: Nix, packageName: nixStorePath.extractFilename.split("-", 1)[^1], casHashes: @[], errors: @[], warnings: @[] ) mm.logger.log(Info, fmt"Migrating Nix package: {nixStorePath}") if not nixStorePath.startsWith("/nix/store/"): result.errors.add("Invalid Nix store path") return if not dirExists(nixStorePath): result.errors.add(fmt"Nix store path not found: {nixStorePath}") return # Migrate files from Nix store for file in walkDirRec(nixStorePath): if not mm.dryRun: let storeResult = mm.casManager.storeFile(file) if storeResult.isOk: result.casHashes.add(storeResult.get().hash) result.success = result.errors.len == 0 # ############################################################################ # Format Conversion Utilities # ############################################################################ proc convertNpkToNip*(mm: var MigrationManager, npkPath: string): MigrationResult = ## Convert an NPK (binary package) to NIP (application) format result = MigrationResult( success: false, source: OldNip, packageName: npkPath.extractFilename.changeFileExt(""), casHashes: @[], errors: @[] ) mm.logger.log(Info, fmt"Converting NPK to NIP: {npkPath}") # NPK is a binary package, NIP is an application bundle # The main difference is the manifest and entry point handling result.warnings.add("NPK->NIP conversion creates application wrapper") result.success = true # Placeholder proc convertNipToNexter*(mm: var MigrationManager, nipPath: string): MigrationResult = ## Convert a NIP to NEXTER container format result = MigrationResult( success: false, source: OldNip, packageName: nipPath.extractFilename, casHashes: @[], errors: @[] ) mm.logger.log(Info, fmt"Converting NIP to NEXTER: {nipPath}") result.warnings.add("NIP->NEXTER creates containerized environment") result.success = true # Placeholder # ############################################################################ # Verification Tools # ############################################################################ proc verifyMigration*(mm: var MigrationManager, migResult: MigrationResult): bool = ## Verify that a migration completed successfully mm.logger.log(Info, fmt"Verifying migration: {migResult.packageName}") if not migResult.success: mm.logger.log(Error, "Migration reported failure") return false # Verify all CAS objects exist var missing = 0 for hash in migResult.casHashes: if not mm.casManager.objectExists(hash): mm.logger.log(Error, fmt"Missing CAS object: {hash}") missing += 1 if missing > 0: mm.logger.log(Error, fmt"{missing} objects missing from CAS") return false mm.logger.log(Info, fmt"Migration verified: {migResult.casHashes.len} objects") return true proc generateMigrationReport*(results: seq[MigrationResult]): string = ## Generate a summary report of migration results var successCount = 0 var failCount = 0 var totalObjects = 0 result = "# Migration Report\n\n" let timestamp = now().format("yyyy-MM-dd HH:mm:ss") result.add fmt"Generated: {timestamp}" & "\n\n" for r in results: if r.success: successCount += 1 totalObjects += r.casHashes.len else: failCount += 1 result.add fmt"## Summary\n\n" result.add fmt"- Total migrations: {results.len}\n" result.add fmt"- Successful: {successCount}\n" result.add fmt"- Failed: {failCount}\n" result.add fmt"- Total CAS objects: {totalObjects}\n\n" result.add "## Details\n\n" for r in results: let status = if r.success: "✅" else: "❌" result.add fmt"### {status} {r.packageName}\n\n" result.add fmt"- Source: {r.source}\n" result.add fmt"- Objects: {r.casHashes.len}\n" if r.errors.len > 0: result.add "- Errors:\n" for err in r.errors: result.add fmt" - {err}\n" if r.warnings.len > 0: result.add "- Warnings:\n" for warn in r.warnings: result.add fmt" - {warn}\n" result.add "\n"