356 lines
13 KiB
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")
|
|
|