nip/src/nimpak/variants.nim

349 lines
9.9 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.
## variants.nim
## Typed variant system for deterministic, content-addressed packages
## Evolution of USE flags into semantic domains with type safety
import std/[tables, strutils, sequtils, algorithm, sets, os]
import config
type
FlagType* = enum
ftBool ## Simple on/off: +lto, -debug
ftChoice ## Mutually exclusive: init=dinit
ftSet ## Multiple allowed: security=pie,relro,hardened
ftEnum ## Predefined options with validation
VariantFlag* = object
domain*: string ## "init", "graphics", "security"
name*: string ## "dinit", "wayland", "pie"
flagType*: FlagType
enabled*: bool
value*: string ## For choice/enum types
affects*: seq[string] ## What this flag impacts
conflicts*: seq[string]
requires*: seq[string]
FlagDomain* = object
name*: string
description*: string
flagType*: FlagType
exclusive*: bool ## Only one flag can be enabled
options*: seq[string]
default*: string ## Default value for choice types
defaults*: seq[string] ## Default values for set types
VariantFingerprint* = object
hash*: string ## BLAKE3 hash of variant configuration
packageName*: string
version*: string
domains*: Table[string, seq[string]]
compilerFlags*: CompilerFlags
toolchain*: string
target*: string
PackageVariant* = object
fingerprint*: VariantFingerprint
installPath*: string
isDefault*: bool
installedAt*: string
# ============================================
# Domain Definitions (9 Semantic Domains)
# ============================================
proc getSemanticDomains*(): Table[string, FlagDomain] =
## Get the 9 semantic domains for typed variants
result = initTable[string, FlagDomain]()
# 1. Init System (exclusive choice)
result["init"] = FlagDomain(
name: "init",
description: "Init system selection",
flagType: ftChoice,
exclusive: true,
options: @["systemd", "dinit", "openrc", "runit", "s6"],
default: "dinit"
)
# 2. Runtime Features (set)
result["runtime"] = FlagDomain(
name: "runtime",
description: "Core runtime features",
flagType: ftSet,
exclusive: false,
options: @[
"ssl", "http3", "zstd", "lz4", "ipv6",
"dbus", "doc", "examples",
"python", "ruby", "perl", "lua", "go", "rust",
"cuda", "rocm", "onnx", "tensorrt",
"steam", "wine", "proton"
],
defaults: @["ssl", "ipv6"]
)
# 3. Graphics (choice + sub-features)
result["graphics"] = FlagDomain(
name: "graphics",
description: "Display server and GPU API",
flagType: ftChoice,
exclusive: false, # Can have display + GPU APIs
options: @[
"none", "X", "wayland",
"vulkan", "opengl", "mesa",
"nvidia", "amd", "intel-gpu"
],
default: "none"
)
# 4. Audio System (exclusive choice)
result["audio"] = FlagDomain(
name: "audio",
description: "Sound server selection",
flagType: ftChoice,
exclusive: true,
options: @["none", "pipewire", "pulseaudio", "alsa", "jack", "oss"],
default: "pipewire"
)
# 5. Security Hardening (set)
result["security"] = FlagDomain(
name: "security",
description: "Security hardening features",
flagType: ftSet,
exclusive: false,
options: @["pie", "relro", "hardened", "fortify", "stack-protector"],
defaults: @["pie", "relro"]
)
# 6. Optimization (set with conflicts)
result["optimization"] = FlagDomain(
name: "optimization",
description: "Build optimizations",
flagType: ftSet,
exclusive: false,
options: @["lto", "pgo", "march-native", "debug", "strip"],
defaults: @["lto"]
)
# 7. Integration (set)
result["integration"] = FlagDomain(
name: "integration",
description: "System interfaces and integration",
flagType: ftSet,
exclusive: false,
options: @[
"docker", "podman", "nipcells", "containerd", "runc", "crun",
"kvm", "qemu", "libvirt", "xen", "bhyve",
"nexus-api", "nexus-db", "nexus-sync", "nexus-monitor", "nexus-security"
],
defaults: @[]
)
# 8. Network (set)
result["network"] = FlagDomain(
name: "network",
description: "Networking stack and protocols",
flagType: ftSet,
exclusive: false,
options: @[
"ipv6", "wireguard", "zerotier", "tailscale",
"mesh", "p2p", "ipfs", "libp2p"
],
defaults: @["ipv6"]
)
# 9. Developer Tools (set)
result["developer"] = FlagDomain(
name: "developer",
description: "Development tools and features",
flagType: ftSet,
exclusive: false,
options: @["debugger", "profiler", "lsp", "repl", "hot-reload", "sanitizer", "coverage"],
defaults: @[]
)
# ============================================
# Variant Flag Parsing
# ============================================
proc parseVariantFlag*(flagStr: string): VariantFlag =
## Parse domain-scoped variant flag
## Examples:
## "+init=dinit"
## "+runtime=ssl,http3,zstd"
## "+optimization=lto"
## "-debug"
let trimmed = flagStr.strip()
if trimmed.len == 0:
raise newException(ValueError, "Empty variant flag")
# Determine enabled/disabled
var enabled = true
var flagPart = trimmed
if trimmed[0] == '+':
enabled = true
flagPart = trimmed[1..^1]
elif trimmed[0] == '-':
enabled = false
flagPart = trimmed[1..^1]
# Check for domain syntax: domain=value
if '=' in flagPart:
let parts = flagPart.split('=', 1)
result = VariantFlag(
domain: parts[0],
name: parts[1],
flagType: ftChoice, # Will be determined by domain
enabled: enabled,
value: parts[1]
)
else:
# Simple flag (backward compatible)
result = VariantFlag(
domain: "", # Will be inferred
name: flagPart,
flagType: ftBool,
enabled: enabled,
value: ""
)
proc inferDomain*(flagName: string, domains: Table[string, FlagDomain]): string =
## Infer domain from flag name for backward compatibility
for domainName, domain in domains:
if flagName in domain.options:
return domainName
return "runtime" # Default domain
# ============================================
# Variant Fingerprint Generation
# ============================================
proc calculateVariantFingerprint*(
packageName: string,
version: string,
domains: Table[string, seq[string]],
compilerFlags: CompilerFlags,
toolchain: string = "default",
target: string = "native"
): string =
## Calculate deterministic BLAKE3 hash for variant
## Hash of: source + version + flags + toolchain + target
var hashInput = ""
# 1. Package identity
hashInput.add(packageName & "\n")
hashInput.add(version & "\n")
# 2. Sorted domain flags (deterministic)
var sortedDomainNames = domains.keys.toSeq
sortedDomainNames.sort()
for domainName in sortedDomainNames:
hashInput.add(domainName & ":")
var sortedFlags = domains[domainName]
sortedFlags.sort()
hashInput.add(sortedFlags.join(",") & "\n")
# 3. Compiler flags
hashInput.add("cflags:" & compilerFlags.cflags & "\n")
hashInput.add("cxxflags:" & compilerFlags.cxxflags & "\n")
hashInput.add("ldflags:" & compilerFlags.ldflags & "\n")
# 4. Toolchain
hashInput.add("toolchain:" & toolchain & "\n")
# 5. Target
hashInput.add("target:" & target & "\n")
# Calculate hash (simplified for now - will use BLAKE3)
# For now, use a simple hash
var simpleHash = 0
for c in hashInput:
simpleHash = (simpleHash * 31 + ord(c)) and 0x7FFFFFFF
result = "blake3-" & simpleHash.toHex()[0..11].toLower()
proc generateVariantPath*(
packageName: string,
version: string,
fingerprint: string,
baseDir: string = "/Programs"
): string =
## Generate variant installation path
## Format: /Programs/<Name>/<Version>-<Fingerprint>/
result = baseDir / packageName / (version & "-" & fingerprint)
# ============================================
# Variant Management
# ============================================
proc listVariants*(packageName: string, baseDir: string = "/Programs"): seq[PackageVariant] =
## List all installed variants of a package
result = @[]
let packageDir = baseDir / packageName
if not dirExists(packageDir):
return
for kind, path in walkDir(packageDir):
if kind == pcDir:
let dirName = path.splitPath().tail
if dirName.contains("-blake3-"):
let parts = dirName.split("-blake3-", 1)
if parts.len == 2:
result.add(PackageVariant(
fingerprint: VariantFingerprint(
hash: "blake3-" & parts[1],
packageName: packageName,
version: parts[0]
),
installPath: path,
isDefault: false, # TODO: Check symlinks
installedAt: ""
))
proc getDefaultVariant*(packageName: string): string =
## Get the default variant fingerprint for a package
## Checks which variant is currently symlinked
# TODO: Implement by checking /System/Links/ symlinks
result = ""
proc setDefaultVariant*(packageName: string, fingerprint: string): bool =
## Set which variant is the default (symlinked)
## Updates symlinks in /System/Links/
# TODO: Implement symlink switching
result = false
# ============================================
# Display Functions
# ============================================
proc displayVariant*(variant: PackageVariant) =
## Display variant information
echo variant.fingerprint.packageName & " " & variant.fingerprint.version & "-" & variant.fingerprint.hash
if variant.isDefault:
echo " [DEFAULT]"
echo " Path: " & variant.installPath
proc displayVariants*(variants: seq[PackageVariant]) =
## Display list of variants
if variants.len == 0:
echo "No variants found"
return
echo "Installed Variants:"
echo ""
for variant in variants:
displayVariant(variant)
echo ""