nip/src/nip/system_integration.nim

356 lines
13 KiB
Nim

## System Integration - NPK Installation & System Management
##
## This module implements the "Physical" layer of the package manager.
## It takes abstract PackageManifests and CAS objects and materializes them
## into the GoboLinux-style directory hierarchy.
##
## Directory Structure:
## - /Programs/<Package>/<Version>/<Hash>/ (Installation Root)
## - /Programs/<Package>/Current (Symlink to active version)
## - /System/Index/bin/ (Symlinks to executables)
## - /System/Index/lib/ (Symlinks to libraries)
##
## Responsibilities:
## 1. File reconstruction from CAS
## 2. Symlink management
## 3. User/Group creation
## 4. Service file generation
import std/[os, posix, strutils, strformat, options, osproc, logging]
import nip/manifest_parser
import nip/cas
import nip/types # For Multihash if needed
type
SystemIntegrator* = ref object
casRoot*: string
programsRoot*: string
systemIndexRoot*: string
dryRun*: bool
proc newSystemIntegrator*(casRoot, programsRoot, systemIndexRoot: string, dryRun: bool = false): SystemIntegrator =
result = SystemIntegrator(
casRoot: casRoot,
programsRoot: programsRoot,
systemIndexRoot: systemIndexRoot,
dryRun: dryRun
)
proc log(si: SystemIntegrator, msg: string) =
if si.dryRun:
echo "[DRY-RUN] " & msg
else:
info(msg)
# ============================================================================
# File Reconstruction
# ============================================================================
proc reconstructFiles(si: SystemIntegrator, manifest: PackageManifest, installDir: string) =
## Reconstruct files from CAS chunks into the installation directory
si.log(fmt"Reconstructing files for {manifest.name} v{manifest.version} in {installDir}")
if not si.dryRun:
createDir(installDir)
for file in manifest.files:
let destPath = installDir / file.path
let destDir = destPath.parentDir
if not si.dryRun:
createDir(destDir)
# In a real implementation, we might have multiple chunks per file.
# For now, we assume 1-to-1 mapping or that CAS handles retrieval transparently.
# manifest_parser uses string for hash, cas uses Multihash.
# We assume 'file.hash' is the CAS object hash.
try:
if not si.dryRun:
# Retrieve content from CAS
# Note: cas.retrieveObject takes Multihash
let content = retrieveObject(Multihash(file.hash), si.casRoot)
writeFile(destPath, content)
# Set permissions
# Parse octal string e.g. "755"
var perms: set[FilePermission] = {}
if file.permissions.len == 3:
let user = file.permissions[0].ord - '0'.ord
let group = file.permissions[1].ord - '0'.ord
let other = file.permissions[2].ord - '0'.ord
if (user and 4) != 0: perms.incl(fpUserRead)
if (user and 2) != 0: perms.incl(fpUserWrite)
if (user and 1) != 0: perms.incl(fpUserExec)
if (group and 4) != 0: perms.incl(fpGroupRead)
if (group and 2) != 0: perms.incl(fpGroupWrite)
if (group and 1) != 0: perms.incl(fpGroupExec)
if (other and 4) != 0: perms.incl(fpOthersRead)
if (other and 2) != 0: perms.incl(fpOthersWrite)
if (other and 1) != 0: perms.incl(fpOthersExec)
setFilePermissions(destPath, perms)
# Add CAS reference
# refId = package:version
let refId = fmt"{manifest.name}:{manifest.version}"
addReference(si.casRoot, Multihash(file.hash), "npk", refId)
except Exception as e:
error(fmt"Failed to reconstruct file {file.path}: {e.msg}")
raise
# ============================================================================
# Symlink Management
# ============================================================================
proc createSymlinks(si: SystemIntegrator, manifest: PackageManifest, installDir: string) =
## Create system links in /System/Index
si.log(fmt"Creating symlinks for {manifest.name}")
# 1. Update 'Current' link
let packageRoot = si.programsRoot / manifest.name
let currentLink = packageRoot / "Current"
if not si.dryRun:
createDir(packageRoot)
# Atomic symlink update would be better, but for MVP:
if symlinkExists(currentLink) or fileExists(currentLink):
removeFile(currentLink)
createSymlink(installDir, currentLink)
# 2. Link binaries to /System/Index/bin
let binDir = installDir / "bin"
let systemBin = si.systemIndexRoot / "bin"
if dirExists(binDir):
if not si.dryRun: createDir(systemBin)
for kind, path in walkDir(binDir):
if kind == pcFile or kind == pcLinkToFile:
let filename = path.extractFilename
let target = systemBin / filename
si.log(fmt"Linking {filename} -> {target}")
if not si.dryRun:
if symlinkExists(target) or fileExists(target):
# Conflict resolution strategy: Overwrite? Warn?
# For now, overwrite
removeFile(target)
# Link to the 'Current' path, not the specific version path,
# so upgrades don't break links if 'Current' is updated.
# Target: /Programs/<Pkg>/Current/bin/<file>
let persistentPath = currentLink / "bin" / filename
createSymlink(persistentPath, target)
# 3. Link libraries to /System/Index/lib
let libDir = installDir / "lib"
let systemLib = si.systemIndexRoot / "lib"
if dirExists(libDir):
if not si.dryRun: createDir(systemLib)
for kind, path in walkDir(libDir):
if kind == pcFile or kind == pcLinkToFile:
let filename = path.extractFilename
# Only link .so files or similar? Or everything?
# GoboLinux links everything usually.
let target = systemLib / filename
si.log(fmt"Linking {filename} -> {target}")
if not si.dryRun:
if symlinkExists(target) or fileExists(target):
removeFile(target)
let persistentPath = currentLink / "lib" / filename
createSymlink(persistentPath, target)
# TODO: Handle share, include, etc.
# ============================================================================
# User/Group Management
# ============================================================================
proc manageUsersGroups(si: SystemIntegrator, manifest: PackageManifest) =
## Create users and groups defined in the manifest
# Groups first
for group in manifest.groups:
si.log(fmt"Ensuring group exists: {group.name}")
if not si.dryRun:
# Check if group exists
let checkCmd = fmt"getent group {group.name}"
if execCmd(checkCmd) != 0:
# Create group
var cmd = fmt"groupadd {group.name}"
if group.gid.isSome:
cmd.add(fmt" -g {group.gid.get()}")
if execCmd(cmd) != 0:
error(fmt"Failed to create group {group.name}")
# Users
for user in manifest.users:
si.log(fmt"Ensuring user exists: {user.name}")
if not si.dryRun:
# Check if user exists
let checkCmd = fmt"getent passwd {user.name}"
if execCmd(checkCmd) != 0:
# Create user
var cmd = fmt"useradd -m -s {user.shell} -d {user.home}"
if user.uid.isSome:
cmd.add(fmt" -u {user.uid.get()}")
if user.group != "":
cmd.add(fmt" -g {user.group}")
cmd.add(fmt" {user.name}")
if execCmd(cmd) != 0:
error(fmt"Failed to create user {user.name}")
# ============================================================================
# Service Management
# ============================================================================
proc manageServices(si: SystemIntegrator, manifest: PackageManifest) =
## Generate and install system service files
let systemdDir = si.systemIndexRoot / "lib/systemd/system"
if manifest.services.len > 0:
if not si.dryRun: createDir(systemdDir)
for service in manifest.services:
let serviceFile = systemdDir / (service.name & ".service")
si.log(fmt"Installing service: {service.name}")
if not si.dryRun:
writeFile(serviceFile, service.content)
if service.enabled:
# Enable service (symlink to multi-user.target.wants usually)
# For MVP, we just run systemctl enable
discard execCmd(fmt"systemctl enable {service.name}")
# ============================================================================
# Main Installation Procedure
# ============================================================================
proc installPackage*(si: SystemIntegrator, manifest: PackageManifest) =
## Main entry point for installing a package
info(fmt"Installing {manifest.name} v{manifest.version}")
# 1. Determine installation path
# /Programs/<Name>/<Version>
# We might want to include hash in path to allow multiple builds of same version?
# Task says: /Programs/App/Version/Hash
let installDir = si.programsRoot / manifest.name / $manifest.version / manifest.artifactHash
if dirExists(installDir):
warn(fmt"Package version already installed at {installDir}")
# Proceed anyway to repair/update? Or return?
# For now, proceed (idempotent)
# 2. Reconstruct files
si.reconstructFiles(manifest, installDir)
# 3. Create Users/Groups
si.manageUsersGroups(manifest)
# 4. Create Symlinks (activates the package)
si.createSymlinks(manifest, installDir)
# 5. Manage Services
si.manageServices(manifest)
# ============================================================================
# Removal Procedure
# ============================================================================
proc removePackage*(si: SystemIntegrator, manifest: PackageManifest) =
## Remove an installed package
info(fmt"Removing {manifest.name} v{manifest.version}")
let installDir = si.programsRoot / manifest.name / $manifest.version / manifest.artifactHash
let currentLink = si.programsRoot / manifest.name / "Current"
# 1. Stop and Disable Services
if manifest.services.len > 0:
for service in manifest.services:
si.log(fmt"Stopping/Disabling service: {service.name}")
if not si.dryRun:
discard execCmd(fmt"systemctl stop {service.name}")
discard execCmd(fmt"systemctl disable {service.name}")
let serviceFile = si.systemIndexRoot / "lib/systemd/system" / (service.name & ".service")
if fileExists(serviceFile):
removeFile(serviceFile)
# 2. Remove Symlinks from /System/Index
# We need to know what files were linked.
# Strategy: Check if 'Current' points to the version we are removing.
# If so, we should remove the links.
# If 'Current' points to another version, we should NOT remove links (except maybe cleaning up orphans?)
var isCurrent = false
if symlinkExists(currentLink):
let target = expandSymlink(currentLink)
if target == installDir:
isCurrent = true
if isCurrent:
si.log("Removing system symlinks")
# Binaries
if dirExists(installDir / "bin"):
for kind, path in walkDir(installDir / "bin"):
let filename = path.extractFilename
let target = si.systemIndexRoot / "bin" / filename
if not si.dryRun and (symlinkExists(target) or fileExists(target)):
removeFile(target)
# Libraries
if dirExists(installDir / "lib"):
for kind, path in walkDir(installDir / "lib"):
let filename = path.extractFilename
let target = si.systemIndexRoot / "lib" / filename
if not si.dryRun and (symlinkExists(target) or fileExists(target)):
removeFile(target)
# Remove 'Current' link
if not si.dryRun:
removeFile(currentLink)
# 3. Remove Installation Directory
if dirExists(installDir):
si.log(fmt"Removing installation directory: {installDir}")
if not si.dryRun:
removeDir(installDir)
# Remove version dir if empty
let versionDir = installDir.parentDir
if dirExists(versionDir):
var versionEmpty = true
for _ in walkDir(versionDir):
versionEmpty = false
break
if versionEmpty:
removeDir(versionDir)
# Remove package dir if empty (no other versions)
let packageDir = si.programsRoot / manifest.name
if dirExists(packageDir):
var isEmpty = true
for _ in walkDir(packageDir):
isEmpty = false
break
if isEmpty:
removeDir(packageDir)
# 4. Remove CAS References
si.log("Removing CAS references")
if not si.dryRun:
let refId = fmt"{manifest.name}:{manifest.version}"
for file in manifest.files:
removeReference(si.casRoot, Multihash(file.hash), "npk", refId)
info(fmt"Removal of {manifest.name} complete")