nip/src/nimpak/install.nim

236 lines
8.1 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/install.nim
# Package installation orchestrator with atomic operations
import std/[tables, sequtils, strformat]
import ./types, dependency, transactions, filesystem, cas
type
InstallStep* = object
package*: PackageId
fragment*: Fragment
stepNumber*: int
totalSteps*: int
InstallPlan* = object
steps*: seq[InstallStep]
transaction*: Transaction
rollbackData*: seq[RollbackInfo]
InstallProgress* = object
currentStep*: int
totalSteps*: int
currentPackage*: PackageId
status*: InstallStatus
InstallStatus* = enum
Planning, Installing, Completed, Failed, RolledBack
InstallError* = object of NimPakError
failedPackage*: PackageId
failedStep*: int
rollbackSuccess*: bool
# Public API
proc installPackages*(packages: seq[PackageId], fragments: Table[PackageId, Fragment],
fsManager: FilesystemManager, casManager: CasManager): Result[void, InstallError] =
## Main installation orchestrator (6.2.1, 6.2.2, 6.2.3, 6.2.4)
# Create installation plan
let planResult = createInstallPlan(packages, fragments)
if planResult.isErr:
return err(InstallError(
code: DependencyConflict,
msg: "Failed to create installation plan: " & planResult.error.msg
))
var plan = planResult.get()
# Begin atomic transaction
plan.transaction = beginTransaction()
# Execute installation steps
let executeResult = executeInstallPlan(plan, fsManager, casManager)
if executeResult.isErr:
# Rollback on failure
let rollbackResult = rollbackTransaction(plan.transaction)
return err(InstallError(
code: executeResult.error.code,
msg: executeResult.error.msg,
failedPackage: executeResult.error.failedPackage,
failedStep: executeResult.error.failedStep,
rollbackSuccess: rollbackResult.isOk
))
# Commit transaction
let commitResult = commitTransaction(plan.transaction)
if commitResult.isErr:
return err(InstallError(
code: TransactionFailed,
msg: "Failed to commit installation: " & commitResult.error
))
ok()
proc createInstallPlan(packages: seq[PackageId], fragments: Table[PackageId, Fragment]): Result[InstallPlan, DependencyError] =
## Create ordered installation plan from dependency resolution (6.2.1)
var allPackages: seq[PackageId] = @[]
var processedPackages = initHashSet[PackageId]()
# Resolve dependencies for each requested package
for pkg in packages:
let resolveResult = resolveDependencies(pkg, fragments)
if resolveResult.isErr:
return err(resolveResult.error)
let installOrder = resolveResult.get()
for orderedPkg in installOrder.packages:
if orderedPkg notin processedPackages:
allPackages.add(orderedPkg)
processedPackages.incl(orderedPkg)
# Create installation steps
var steps: seq[InstallStep] = @[]
for i, pkg in allPackages.pairs:
if pkg in fragments:
steps.add(InstallStep(
package: pkg,
fragment: fragments[pkg],
stepNumber: i + 1,
totalSteps: allPackages.len
))
ok(InstallPlan(
steps: steps,
transaction: Transaction(), # Will be initialized later
rollbackData: @[]
))
proc executeInstallPlan(plan: var InstallPlan, fsManager: FilesystemManager,
casManager: CasManager): Result[void, InstallError] =
## Execute installation plan with progress tracking (6.2.3, 6.2.5)
for step in plan.steps:
echo fmt"Installing {step.package.name} ({step.stepNumber}/{step.totalSteps})"
# Install individual package
let installResult = installSinglePackage(step, fsManager, casManager, plan.transaction)
if installResult.isErr:
return err(InstallError(
code: installResult.error.code,
msg: fmt"Failed to install {step.package.name}: {installResult.error.msg}",
failedPackage: step.package,
failedStep: step.stepNumber
))
ok()
proc installSinglePackage(step: InstallStep, fsManager: FilesystemManager,
casManager: CasManager, transaction: var Transaction): Result[void, NimPakError] =
## Install a single package with atomic operations
let pkg = step.package
let fragment = step.fragment
# Create package directory structure
let programDir = fmt"/Programs/{pkg.name}/{pkg.version}"
let createDirOp = Operation(
kind: CreateDir,
target: programDir,
data: %*{"permissions": "755"}
)
transaction.addOperation(createDirOp)
# Install package files from CAS or source
let installResult = installPackageFiles(fragment, programDir, casManager)
if installResult.isErr:
return installResult
# Create symlinks in /System/Index
let symlinkResult = createPackageSymlinks(fragment, programDir, fsManager, transaction)
if symlinkResult.isErr:
return symlinkResult
ok()
proc installPackageFiles(fragment: Fragment, targetDir: string, casManager: CasManager): Result[void, NimPakError] =
## Install package files from CAS or extract from source
# TODO: Implement file extraction from CAS or NPK package
# For now, create placeholder implementation
echo fmt"Installing files for {fragment.id.name} to {targetDir}"
ok()
proc createPackageSymlinks(fragment: Fragment, programDir: string,
fsManager: FilesystemManager, transaction: var Transaction): Result[void, NimPakError] =
## Create symlinks in /System/Index for package binaries and libraries
# TODO: Implement symlink creation based on package manifest
# For now, create placeholder implementation
echo fmt"Creating symlinks for {fragment.id.name}"
ok()
# Progress tracking utilities (6.2.5)
proc getInstallProgress*(plan: InstallPlan, currentStep: int): InstallProgress =
## Get current installation progress
let current = if currentStep <= plan.steps.len: currentStep else: plan.steps.len
let currentPkg = if current > 0 and current <= plan.steps.len:
plan.steps[current - 1].package
else:
PackageId(name: "", version: "", stream: Stable)
InstallProgress(
currentStep: current,
totalSteps: plan.steps.len,
currentPackage: currentPkg,
status: if current == plan.steps.len: Completed else: Installing
)
proc formatInstallProgress*(progress: InstallProgress): string =
## Format installation progress for display
let percentage = if progress.totalSteps > 0:
(progress.currentStep * 100) div progress.totalSteps
else: 0
fmt"[{percentage:3}%] Installing {progress.currentPackage.name} ({progress.currentStep}/{progress.totalSteps})"
# Parallel installation support (6.2.6 - future enhancement)
proc installPackagesParallel*(packages: seq[PackageId], fragments: Table[PackageId, Fragment],
fsManager: FilesystemManager, casManager: CasManager): Result[void, InstallError] =
## Parallel installation of independent package subtrees (future enhancement)
# TODO: Implement parallel installation using spawn for independent subtrees
# For now, fall back to sequential installation
installPackages(packages, fragments, fsManager, casManager)
# Utility functions
proc validateInstallPlan*(plan: InstallPlan): Result[void, InstallError] =
## Validate installation plan before execution
if plan.steps.len == 0:
return err(InstallError(
code: InvalidOperation,
msg: "Installation plan is empty"
))
# Check for duplicate packages
var seen = initHashSet[PackageId]()
for step in plan.steps:
if step.package in seen:
return err(InstallError(
code: DependencyConflict,
msg: fmt"Duplicate package in plan: {step.package.name}"
))
seen.incl(step.package)
ok()
proc getInstallSummary*(plan: InstallPlan): string =
## Generate installation summary
result = fmt"Installation Plan Summary:\n"
result.add(fmt"Total packages: {plan.steps.len}\n")
result.add("Installation order:\n")
for step in plan.steps:
result.add(fmt" {step.stepNumber}. {step.package.name} {step.package.version}\n")