236 lines
8.1 KiB
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") |