Files
score-system/.devenv/bootstrap/bootstrapLib.nix
2026-06-22 12:07:31 +01:00

604 lines
21 KiB
Nix

# Shared library functions for devenv evaluation
{ inputs }:
rec {
# Helper to get overlays for a given input
getOverlays =
inputName: inputAttrs:
let
lib = inputs.nixpkgs.lib;
in
map
(
overlay:
let
input =
inputs.${inputName} or (throw "No such input `${inputName}` while trying to configure overlays.");
in
input.overlays.${overlay}
or (throw "Input `${inputName}` has no overlay called `${overlay}`. Supported overlays: ${lib.concatStringsSep ", " (builtins.attrNames input.overlays)}")
) inputAttrs.overlays or [ ];
# Main function to create devenv configuration for a specific system with profiles support
# This is the full-featured version used by default.nix
mkDevenvForSystem =
{ version
, is_development_version ? false
, require_version_match ? false
, system
, devenv_root
, git_root ? null
, devenv_dotfile
, devenv_dotfile_path
, devenv_tmpdir
, devenv_runtime
, devenv_state ? null
, devenv_istesting ? false
, devenv_direnvrc_latest_version
, active_profiles ? [ ]
, hostname
, username
, cli_options ? [ ]
, skip_local_src ? false
, secretspec ? null
, devenv_inputs ? { }
, devenv_imports ? [ ]
, impure ? false
, nixpkgs_config ? { }
, lock_fingerprint ? null
, primops ? { }
}:
let
inherit (inputs) nixpkgs;
lib = nixpkgs.lib;
targetSystem = system;
overlays = lib.flatten (lib.mapAttrsToList getOverlays devenv_inputs);
# Helper to create pkgs for a given system with nixpkgs_config
mkPkgsForSystem =
evalSystem:
import nixpkgs {
system = evalSystem;
config = nixpkgs_config // {
# nixpkgs' check-meta.nix natively handles permittedInsecurePackages
# via allowInsecureDefaultPredicate using the full derivation name.
# We must NOT override allowInsecurePredicate here, as lib.getName
# strips the version, causing mismatches with user-provided entries
# like "openssl-1.1.1w".
#
# For unfree packages, nixpkgs does not natively support
# permittedUnfreePackages, so we provide a custom predicate.
allowUnfreePredicate =
if nixpkgs_config.allowUnfree or false then
(_: true)
else if (nixpkgs_config.permittedUnfreePackages or [ ]) != [ ] then
(pkg: builtins.elem (lib.getName pkg) (nixpkgs_config.permittedUnfreePackages or [ ]))
else
(_: false);
} // lib.optionalAttrs ((nixpkgs_config.allowlistedLicenses or [ ]) != [ ]) {
allowlistedLicenses = map (name: lib.licenses.${name}) (nixpkgs_config.allowlistedLicenses or [ ]);
} // lib.optionalAttrs ((nixpkgs_config.blocklistedLicenses or [ ]) != [ ]) {
blocklistedLicenses = map (name: lib.licenses.${name}) (nixpkgs_config.blocklistedLicenses or [ ]);
};
inherit overlays;
};
pkgsBootstrap = mkPkgsForSystem targetSystem;
# Helper to import a path, trying .nix first then /devenv.nix
# Returns a list of modules, including devenv.local.nix when present
tryImport =
resolvedPath: basePath:
if lib.hasSuffix ".nix" basePath then
[ (import resolvedPath) ]
else
let
devenvpath = resolvedPath + "/devenv.nix";
localpath = resolvedPath + "/devenv.local.nix";
in
if builtins.pathExists devenvpath then
[ (import devenvpath) ] ++ lib.optional (builtins.pathExists localpath) (import localpath)
else
throw (basePath + "/devenv.nix file does not exist");
importModule =
path:
if lib.hasPrefix "path:" path then
# path: prefix indicates a local filesystem path - strip it and import directly
let
actualPath = builtins.substring 5 999999 path;
in
tryImport (/. + actualPath) path
else if lib.hasPrefix "/" path then
# Absolute path - import directly (avoids input resolution and NAR hash computation)
tryImport (/. + path) path
else if lib.hasPrefix "./" path then
# Relative paths are relative to devenv_root, not bootstrap directory
let
relPath = builtins.substring 1 255 path;
in
tryImport (/. + devenv_root + relPath) path
else if lib.hasPrefix "../" path then
# Parent relative paths also relative to devenv_root
tryImport (/. + devenv_root + "/${path}") path
else
let
paths = lib.splitString "/" path;
name = builtins.head paths;
input = inputs.${name} or (throw "Unknown input ${name}");
subpath = "/${lib.concatStringsSep "/" (builtins.tail paths)}";
devenvpath = input + subpath;
in
tryImport devenvpath path;
# Common modules shared between main evaluation and cross-system evaluation
mkCommonModules =
evalPkgs:
[
(
{ config, ... }:
{
_module.args.pkgs = evalPkgs.appendOverlays (config.overlays or [ ]);
_module.args.secretspec = secretspec;
_module.args.devenvPrimops = primops;
}
)
(inputs.devenv.modules + /top-level.nix)
(
{ options, ... }:
{
config.devenv = lib.mkMerge [
{
root = devenv_root;
dotfile = devenv_dotfile;
}
(
if builtins.hasAttr "cli" options.devenv then
{
cli.version = version;
cli.isDevelopment = is_development_version;
}
else
{
cliVersion = version;
}
)
(lib.optionalAttrs
(builtins.hasAttr "cli" options.devenv
&& builtins.hasAttr "requireVersionMatch" options.devenv.cli)
{
cli.requireVersionMatch = require_version_match;
}
)
(lib.optionalAttrs (builtins.hasAttr "tmpdir" options.devenv) {
tmpdir = devenv_tmpdir;
})
(lib.optionalAttrs (builtins.hasAttr "isTesting" options.devenv) {
isTesting = devenv_istesting;
})
(lib.optionalAttrs (builtins.hasAttr "runtime" options.devenv) {
runtime = devenv_runtime;
})
(lib.optionalAttrs (builtins.hasAttr "state" options.devenv && devenv_state != null) {
state = lib.mkForce devenv_state;
})
(lib.optionalAttrs (builtins.hasAttr "direnvrcLatestVersion" options.devenv) {
direnvrcLatestVersion = devenv_direnvrc_latest_version;
})
];
}
)
(
{ options, ... }:
{
config = lib.mkMerge [
(lib.optionalAttrs (builtins.hasAttr "git" options) {
git.root = git_root;
})
];
}
)
]
++ (lib.flatten (map importModule devenv_imports))
++ (if !skip_local_src then (importModule (devenv_root + "/devenv.nix")) else [ ])
++ [
(
let
localPath = devenv_root + "/devenv.local.nix";
in
if builtins.pathExists localPath then import localPath else { }
)
cli_options
];
# Phase 1: Base evaluation to extract profile definitions
baseProject = lib.evalModules {
specialArgs = inputs // {
inherit inputs secretspec primops;
};
modules = mkCommonModules pkgsBootstrap;
};
# Phase 2: Extract and apply profiles using extendModules with priority overrides
project =
let
# Build ordered list of profile names: hostname -> user -> manual
manualProfiles = active_profiles;
currentHostname = hostname;
currentUsername = username;
hostnameProfiles = lib.optional
(
currentHostname != null
&& currentHostname != ""
&& builtins.hasAttr currentHostname (baseProject.config.profiles.hostname or { })
) "hostname.${currentHostname}";
userProfiles = lib.optional
(
currentUsername != null
&& currentUsername != ""
&& builtins.hasAttr currentUsername (baseProject.config.profiles.user or { })
) "user.${currentUsername}";
# Ordered list of profiles to activate
orderedProfiles = hostnameProfiles ++ userProfiles ++ manualProfiles;
# Resolve profile extends with cycle detection
resolveProfileExtends =
profileName: visited:
if builtins.elem profileName visited then
throw "Circular dependency detected in profile extends: ${lib.concatStringsSep " -> " visited} -> ${profileName}"
else
let
profile = getProfileConfig profileName;
extends = profile.extends or [ ];
newVisited = visited ++ [ profileName ];
extendedProfiles = lib.flatten (map (name: resolveProfileExtends name newVisited) extends);
in
extendedProfiles ++ [ profileName ];
# Get profile configuration by name from baseProject
getProfileConfig =
profileName:
if lib.hasPrefix "hostname." profileName then
let
name = lib.removePrefix "hostname." profileName;
in
baseProject.config.profiles.hostname.${name}
else if lib.hasPrefix "user." profileName then
let
name = lib.removePrefix "user." profileName;
in
baseProject.config.profiles.user.${name}
else
let
availableProfiles = builtins.attrNames (baseProject.config.profiles or { });
hostnameProfiles = map (n: "hostname.${n}") (
builtins.attrNames (baseProject.config.profiles.hostname or { })
);
userProfiles = map (n: "user.${n}") (builtins.attrNames (baseProject.config.profiles.user or { }));
allAvailableProfiles = availableProfiles ++ hostnameProfiles ++ userProfiles;
in
baseProject.config.profiles.${profileName}
or (throw "Profile '${profileName}' not found. Available profiles: ${lib.concatStringsSep ", " allAvailableProfiles}");
# Fold over ordered profiles to build final list with extends
expandedProfiles = lib.foldl'
(
acc: profileName:
let
allProfileNames = resolveProfileExtends profileName [ ];
in
acc ++ allProfileNames
) [ ]
orderedProfiles;
# Map over expanded profiles and apply priorities
allPrioritizedModules = lib.imap0
(
index: profileName:
let
profilePriority = (lib.modules.defaultOverridePriority - 1) - index;
profileConfig = getProfileConfig profileName;
typeNeedsOverride =
type:
if type == null then
false
else
let
typeName = type.name or type._type or "";
isLeafType = builtins.elem typeName [
"str"
"int"
"bool"
"enum"
"path"
"package"
"float"
"anything"
];
in
if isLeafType then
true
else if typeName == "nullOr" then
let
innerType =
type.elemType
or (if type ? nestedTypes && type.nestedTypes ? elemType then type.nestedTypes.elemType else null);
in
if innerType != null then typeNeedsOverride innerType else false
else
false;
pathNeedsOverride =
optionPath:
let
directOption = lib.attrByPath optionPath null baseProject.options;
in
if directOption != null && lib.isOption directOption then
typeNeedsOverride directOption.type
else if optionPath != [ ] then
let
parentPath = lib.init optionPath;
parentOption = lib.attrByPath parentPath null baseProject.options;
in
if parentOption != null && lib.isOption parentOption then
let
freeformType = parentOption.type.freeformType or parentOption.type.nestedTypes.freeformType or null;
elementType =
if freeformType ? elemType then
freeformType.elemType
else if freeformType ? nestedTypes && freeformType.nestedTypes ? elemType then
freeformType.nestedTypes.elemType
else
freeformType;
in
typeNeedsOverride elementType
else
false
else
false;
applyModuleOverride =
config:
if builtins.isFunction config then
let
wrapper = args: applyOverrideRecursive (config args) [ ];
in
lib.mirrorFunctionArgs config wrapper
else
applyOverrideRecursive config [ ];
applyOverrideRecursive =
config: optionPath:
if lib.isAttrs config && config ? _type then
config
else if lib.isAttrs config then
lib.mapAttrs (name: value: applyOverrideRecursive value (optionPath ++ [ name ])) config
else if pathNeedsOverride optionPath then
lib.mkOverride profilePriority config
else
config;
prioritizedConfig = (
profileConfig.module
// {
imports = lib.map
(
importItem:
importItem
// {
imports = lib.map (nestedImport: applyModuleOverride nestedImport) (importItem.imports or [ ]);
}
)
(profileConfig.module.imports or [ ]);
}
);
in
prioritizedConfig
)
expandedProfiles;
in
if allPrioritizedModules == [ ] then
baseProject
else
baseProject.extendModules { modules = allPrioritizedModules; };
config = project.config;
# Per-container scoped re-evaluation that flips `isBuilding` for the
# container being built. Selecting one container cannot pollute the
# evaluation of any other operation, since each `containerBuilds.<name>`
# is its own `extendModules` scope.
mkContainerBuilds =
evalProject:
lib.genAttrs (lib.attrNames evalProject.config.containers) (
name:
let
scoped = evalProject.extendModules {
modules = [{
container.isBuilding = lib.mkForce true;
containers.${name}.isBuilding = lib.mkForce true;
}];
};
in
scoped.config.containers.${name}
);
containerBuilds = mkContainerBuilds project;
# Apply config overlays to pkgs
pkgs = pkgsBootstrap.appendOverlays (config.overlays or [ ]);
options = pkgs.nixosOptionsDoc {
options = builtins.removeAttrs project.options [ "_module" ];
warningsAreErrors = false;
transformOptions =
let
isDocType =
v:
builtins.elem v [
"literalDocBook"
"literalExpression"
"literalMD"
"mdDoc"
];
in
lib.attrsets.mapAttrs (
_: v:
if v ? _type && isDocType v._type then
v.text
else if v ? _type && v._type == "derivation" then
v.name
else
v
);
};
build =
options: config:
lib.concatMapAttrs
(
name: option:
if lib.isOption option then
let
typeName = option.type.name or "";
in
if
builtins.elem typeName [
"output"
"outputOf"
]
then
{
${name} = config.${name};
}
else
{ }
else if builtins.isAttrs option && !lib.isDerivation option then
let
v = build option config.${name};
in
if v != { } then { ${name} = v; } else { }
else
{ }
)
options;
# Helper to evaluate devenv for a specific system (for cross-compilation, e.g. macOS building Linux containers)
evalForSystem =
evalSystem:
let
evalPkgs = mkPkgsForSystem evalSystem;
evalProject = lib.evalModules {
specialArgs = inputs // {
inherit inputs secretspec primops;
};
modules = mkCommonModules evalPkgs;
};
in
{
config = evalProject.config;
containerBuilds = mkContainerBuilds evalProject;
};
# All supported systems for cross-compilation (lazily evaluated)
allSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
# Generate perSystem entries for all systems (only evaluated when accessed)
perSystemConfigs = lib.genAttrs allSystems (
perSystem:
if perSystem == targetSystem then
{ inherit config containerBuilds; }
else
evalForSystem perSystem
);
in
{
inherit
pkgs
config
options
project
;
bash = pkgs.bash;
shell = config.shell;
optionsJSON = options.optionsJSON;
info = config.info;
ci = config.ciDerivation;
build = build project.options config;
devenv = {
# Backwards compatibility: wrap config in devenv attribute for code expecting devenv.config.*
inherit config containerBuilds;
# perSystem structure for cross-compilation (e.g. macOS building Linux containers)
perSystem = perSystemConfigs;
};
};
# Simplified devenv evaluation for inputs
# This is a lightweight version suitable for evaluating an input's devenv.nix
mkDevenvForInput =
{
# The input to evaluate (must have outPath and sourceInfo)
input
, # All resolved inputs (for specialArgs)
allInputs
, # System to evaluate for
system ? builtins.currentSystem
, # Nixpkgs to use (defaults to allInputs.nixpkgs)
nixpkgs ? allInputs.nixpkgs or (throw "nixpkgs input required")
, # Devenv modules (defaults to allInputs.devenv)
devenv ? allInputs.devenv or (throw "devenv input required")
,
}:
let
devenvPath = input.outPath + "/devenv.nix";
hasDevenv = builtins.pathExists devenvPath;
in
if !hasDevenv then
throw ''
Input does not have a devenv.nix file.
Expected file at: ${devenvPath}
To use this input's devenv configuration, the input must provide a devenv.nix file.
''
else
let
pkgs = import nixpkgs {
inherit system;
config = { };
};
lib = pkgs.lib;
project = lib.evalModules {
specialArgs = allInputs // {
inputs = allInputs;
secretspec = null;
};
modules = [
(
{ config, ... }:
{
_module.args.pkgs = pkgs.appendOverlays (config.overlays or [ ]);
}
)
(devenv.outPath + "/src/modules/top-level.nix")
(import devenvPath)
];
};
in
{
inherit pkgs;
config = project.config;
options = project.options;
inherit project;
};
}