593 lines
20 KiB
Nix
593 lines
20 KiB
Nix
/*
|
|
This file implements everything around meta.problems, including:
|
|
- automaticProblems: Which problems get added automatically based on some condition
|
|
- configOptions: Module system options for config.problems
|
|
- problemsType: The check for meta.problems
|
|
- genHandlerSwitch: The logic to determine the handler for a specific problem based on config.problems
|
|
- genCheckProblems: The logic to determine which problems need to be handled and how the messages should look like
|
|
|
|
There are tests to cover pretty much this entire file, so please run them when making changes ;)
|
|
|
|
nix-build -A tests.problems
|
|
*/
|
|
|
|
{ lib }:
|
|
|
|
rec {
|
|
|
|
inherit (lib.strings)
|
|
escapeNixIdentifier
|
|
;
|
|
|
|
inherit (lib)
|
|
any
|
|
listToAttrs
|
|
concatStringsSep
|
|
optionalString
|
|
getName
|
|
optionalAttrs
|
|
pipe
|
|
isString
|
|
filterAttrs
|
|
mapAttrs
|
|
partition
|
|
elemAt
|
|
max
|
|
foldl'
|
|
elem
|
|
filter
|
|
concatMapStringsSep
|
|
optional
|
|
optionals
|
|
concatLists
|
|
all
|
|
attrNames
|
|
attrValues
|
|
length
|
|
mapAttrsToList
|
|
groupBy
|
|
subtractLists
|
|
genAttrs
|
|
concatMap
|
|
unique
|
|
;
|
|
|
|
handlers = rec {
|
|
# Ordered from less to more
|
|
levels = [
|
|
"ignore"
|
|
"warn"
|
|
"error"
|
|
];
|
|
|
|
lessThan =
|
|
a: b:
|
|
if a == "error" then
|
|
false
|
|
else if a == "warn" then
|
|
b == "error"
|
|
else
|
|
b != "ignore";
|
|
|
|
max = a: b: if lessThan a b then b else a;
|
|
};
|
|
|
|
kinds = {
|
|
maintainerless = {
|
|
manualAllowed = false;
|
|
isUnique = false;
|
|
nixpkgsInternalUseAllowed = true;
|
|
automatic = {
|
|
condition =
|
|
# To get usable output, we want to avoid flagging "internal" derivations.
|
|
# Because we do not have a way to reliably decide between internal or
|
|
# external derivation, some heuristics are required to decide.
|
|
#
|
|
# If `outputHash` is defined, the derivation is a FOD, such as the output of a fetcher.
|
|
# If `description` is not defined, the derivation is probably not a package.
|
|
# Simply checking whether `meta` is defined is insufficient,
|
|
# as some fetchers and trivial builders do define meta.
|
|
config: attrs:
|
|
# Order of checks optimised for short-circuiting the common case of having maintainers
|
|
(attrs.meta.maintainers or [ ] == [ ])
|
|
&& (attrs.meta.teams or [ ] == [ ])
|
|
&& (!attrs ? outputHash)
|
|
&& (attrs ? meta.description);
|
|
value.message = "This package has no declared maintainer, i.e. an empty `meta.maintainers` and `meta.teams` attribute.";
|
|
};
|
|
};
|
|
broken = {
|
|
manualAllowed = true;
|
|
isUnique = false;
|
|
nixpkgsInternalUseAllowed = true;
|
|
automatic = {
|
|
condition =
|
|
config:
|
|
let
|
|
# TODO: Consider deprecating this or making it generic for all problems
|
|
allowBroken = config.allowBroken || builtins.getEnv "NIXPKGS_ALLOW_BROKEN" == "1";
|
|
|
|
allowBrokenPredicate =
|
|
lib.warnIf (lib.oldestSupportedReleaseIsAtLeast 2605)
|
|
"config.allowBrokenPredicate is deprecated, use config.problems.handlers.myPackage.broken = \"warn\" for individual packages instead."
|
|
config.allowBrokenPredicate;
|
|
in
|
|
if allowBroken then
|
|
attrs: false
|
|
else if config ? allowBrokenPredicate then
|
|
attrs: attrs ? meta.broken && attrs.meta.broken && !allowBrokenPredicate attrs
|
|
else
|
|
attrs: attrs ? meta.broken && attrs.meta.broken;
|
|
value.message = "This package is broken.";
|
|
};
|
|
};
|
|
removal = {
|
|
manualAllowed = true;
|
|
isUnique = true;
|
|
nixpkgsInternalUseAllowed = false;
|
|
automatic = null;
|
|
};
|
|
deprecated = {
|
|
manualAllowed = true;
|
|
isUnique = false;
|
|
nixpkgsInternalUseAllowed = false;
|
|
automatic = null;
|
|
};
|
|
};
|
|
|
|
# Problem kinds that are currently allowed to be specified in `meta.problems`
|
|
manualKinds = lib.filterAttrs (name: value: value.manualAllowed) kinds;
|
|
# Problem kinds that are currently only allowed to be specified once
|
|
uniqueKinds = lib.filterAttrs (name: value: value.isUnique) kinds;
|
|
|
|
disallowNixpkgsInternalUseKinds = lib.filterAttrs (
|
|
name: value: !value.nixpkgsInternalUseAllowed
|
|
) kinds;
|
|
|
|
automaticProblems = lib.mapAttrsToList (name: value: value.automatic // { kindName = name; }) (
|
|
lib.filterAttrs (name: value: value.automatic != null) kinds
|
|
);
|
|
|
|
genAutomaticProblems =
|
|
config: attrs:
|
|
listToAttrs (
|
|
map (problem: lib.nameValuePair problem.kindName problem.value) (
|
|
filter (problem: problem.condition config attrs) automaticProblems
|
|
)
|
|
);
|
|
|
|
# A module system type for Nixpkgs config
|
|
configOptions =
|
|
let
|
|
types = lib.types;
|
|
handlerType = types.enum handlers.levels;
|
|
problemKindType = types.enum (attrNames kinds);
|
|
in
|
|
{
|
|
handlers = lib.mkOption {
|
|
type = with types; attrsOf (attrsOf handlerType);
|
|
default = { };
|
|
description = ''
|
|
Specify how to handle packages with problems.
|
|
Each key has the format `packageName.problemName`, each value is one of "error", "warn" or "ignore".
|
|
|
|
This option takes precedence over anything in `problems.matchers`.
|
|
|
|
Package names are taken from `lib.getName`, which looks at the `pname` first and falls back to extracting the "pname" part from the `name` attribute.
|
|
|
|
See [Installing packages with problems](https://nixos.org/manual/nixpkgs/stable/#sec-ignore-problems) in the NixOS manual.
|
|
'';
|
|
};
|
|
|
|
matchers = lib.mkOption {
|
|
type = types.listOf (
|
|
types.submodule (
|
|
{ config, ... }:
|
|
{
|
|
options = {
|
|
package = lib.mkOption {
|
|
type = types.nullOr types.str;
|
|
description = "Match problems of packages with this name";
|
|
default = null;
|
|
};
|
|
name = lib.mkOption {
|
|
type = types.nullOr types.str;
|
|
description = "Match problems with this problem name";
|
|
default = null;
|
|
};
|
|
kind = lib.mkOption {
|
|
type = types.nullOr problemKindType;
|
|
description = "Match problems of this problem kind";
|
|
default = null;
|
|
};
|
|
handler = lib.mkOption {
|
|
type = handlerType;
|
|
description = "Specify the handler for matched problems";
|
|
};
|
|
|
|
# Temporary hack to get assertions in submodules, see global assertions below
|
|
assertions = lib.mkOption {
|
|
type = types.listOf types.anything;
|
|
default = [ ];
|
|
internal = true;
|
|
};
|
|
};
|
|
config = {
|
|
assertions =
|
|
# Using optional because otherwise message would be evaluated even when assertion is true
|
|
(
|
|
optional (config.package != null && config.name != null) {
|
|
assertion = false;
|
|
# TODO: Does it really matter if we let people specify this? Maybe not, so consider removing this assertion
|
|
message = ''
|
|
There is a problems.matchers with `package = "${config.package}"` and `name = "${config.name}". Use the following instead:
|
|
problems.handlers.${escapeNixIdentifier config.package}.${escapeNixIdentifier config.name} = "${config.handler}";
|
|
'';
|
|
}
|
|
);
|
|
};
|
|
}
|
|
)
|
|
);
|
|
default = [ ];
|
|
description = ''
|
|
A more powerful and less ergonomic version of `problems.handlers`.
|
|
Each value is a matcher, that may match onto certain properties of a problem and specify a handler for them.
|
|
|
|
If multiple matchers match a problem, the handler with the highest severity (error > warn > ignore) will be used.
|
|
Values in `problems.handlers` always take precedence over matchers.
|
|
|
|
Any matchers must not contain both a `package` and `name` field, for this should be handled by using `problems.handlers` instead.
|
|
'';
|
|
example = [
|
|
{
|
|
kind = "maintainerless";
|
|
handler = "warn";
|
|
}
|
|
{
|
|
package = "myPackageICareAbout";
|
|
handler = "error";
|
|
}
|
|
];
|
|
};
|
|
};
|
|
|
|
# The type for meta.problems
|
|
problemsType =
|
|
let
|
|
types = import ../../../lib/meta-types.nix { inherit lib; };
|
|
inherit (types)
|
|
str
|
|
listOf
|
|
attrsOf
|
|
record
|
|
enum
|
|
;
|
|
# While we should only allow manual kinds, we need to allow `meta.problems = otherPackage.meta.problems`, which includes automatic ones as well
|
|
kindType = enum (attrNames kinds);
|
|
subRecord = record {
|
|
kind = kindType;
|
|
message = str;
|
|
urls = listOf str;
|
|
};
|
|
simpleType = attrsOf subRecord;
|
|
in
|
|
{
|
|
name = "problems";
|
|
verify =
|
|
v:
|
|
v == { }
|
|
||
|
|
simpleType.verify v
|
|
&& all (problem: problem ? message) (attrValues v)
|
|
&& (
|
|
let
|
|
kindGroups = groupBy (kind: kind) (mapAttrsToList (name: problem: problem.kind or name) v);
|
|
in
|
|
all (kind: kinds ? ${kind} && (uniqueKinds ? ${kind} -> length kindGroups.${kind} == 1)) (
|
|
attrNames kindGroups
|
|
)
|
|
);
|
|
errors =
|
|
ctx: v:
|
|
let
|
|
kindGroups = groupBy (attrs: attrs.kind) (
|
|
mapAttrsToList (name: problem: {
|
|
inherit name;
|
|
explicit = problem ? kind;
|
|
kind = problem.kind or name;
|
|
}) v
|
|
);
|
|
in
|
|
if !simpleType.verify v then
|
|
types.errors simpleType ctx v
|
|
else
|
|
concatLists (
|
|
mapAttrsToList (name: p: optional (!p ? message) "${ctx}.${name}: `.message` not specified") v
|
|
)
|
|
++ concatLists (
|
|
mapAttrsToList (
|
|
kind: kindGroup:
|
|
optionals (!kinds ? ${kind}) (
|
|
map (
|
|
el:
|
|
"${ctx}.${el.name}: Problem kind ${kind}, inferred from the problem name, is invalid; expected ${kindType.name}. You can specify an explicit problem kind with `${ctx}.${el.name}.kind`"
|
|
) (filter (el: !el.explicit) kindGroup)
|
|
)
|
|
++
|
|
optional (uniqueKinds ? ${kind} && length kindGroup > 1)
|
|
"${ctx}: Problem kind ${kind} should be unique, but is used for these problems: ${
|
|
concatMapStringsSep ", " (el: el.name) kindGroup
|
|
}"
|
|
) kindGroups
|
|
);
|
|
};
|
|
|
|
/*
|
|
Construct a structure as follows, with the invariant that a more specific path always has a stricter handler, forming a lattice.
|
|
E.g. if `packageSpecific.foo.nameFallback.kindFallback == "warn"`, then `packageFallback.nameFallback.kindFallback` must be "ignore".
|
|
|
|
packageSpecific.<package> = {
|
|
nameSpecific.<name> = {
|
|
kindSpecific.<kind> = <ignore|warn|error>;
|
|
kindFallback = <ignore|warn|error>;
|
|
};
|
|
nameFallback = {
|
|
kindSpecific.<kind> = <ignore|warn|error>;
|
|
kindFallback = <ignore|warn|error>;
|
|
};
|
|
};
|
|
packageFallback = {
|
|
nameSpecific.<name> = {
|
|
kindSpecific.<kind> = <ignore|warn|error>;
|
|
kindFallback = <ignore|warn|error>;
|
|
};
|
|
nameFallback = {
|
|
kindSpecific.<kind> = <ignore|warn|error>;
|
|
kindFallback = <ignore|warn|error>;
|
|
};
|
|
};
|
|
|
|
Returns:
|
|
- the structure itself for inspection
|
|
- a function that can query the structure with very few allocations/lookups
|
|
- a list of problem kinds/names/packages that require handling
|
|
|
|
This allows collapsing arbitrarily many problem handlers/matchers into a predictable structure that can be queried in a predictable and fast way
|
|
*/
|
|
genHandlerSwitch =
|
|
config:
|
|
let
|
|
constraints =
|
|
# matchers have low priority
|
|
map (m: m // { priority = 0; }) config.problems.matchers
|
|
# handlers have higher priority
|
|
++ concatLists (
|
|
mapAttrsToList (
|
|
package: forPackage:
|
|
mapAttrsToList (name: handler: {
|
|
inherit package name handler;
|
|
kind = null;
|
|
priority = 1;
|
|
}) forPackage
|
|
) config.problems.handlers
|
|
);
|
|
|
|
# Lookup table for all the kinds/names/packages that actually need to be
|
|
# handled
|
|
definedConstraints = listToAttrs (
|
|
map (ident: {
|
|
name = "${ident}s"; # plural
|
|
value = unique (
|
|
concatMap (
|
|
constraint:
|
|
optionals (constraint.${ident} != null && constraint.handler != "ignore") [ (constraint.${ident}) ]
|
|
) constraints
|
|
);
|
|
}) identOrder
|
|
);
|
|
|
|
getHandler =
|
|
list:
|
|
(foldl'
|
|
(acc: el: {
|
|
priority = max acc.priority el.priority;
|
|
handler =
|
|
if acc.priority == el.priority then
|
|
handlers.max acc.handler el.handler
|
|
else if acc.priority > el.priority then
|
|
acc.handler
|
|
else
|
|
el.handler;
|
|
})
|
|
{
|
|
priority = 0;
|
|
handler = "ignore";
|
|
}
|
|
list
|
|
).handler;
|
|
|
|
identOrder = [
|
|
"kind"
|
|
"name"
|
|
"package"
|
|
];
|
|
|
|
doLevel =
|
|
index:
|
|
let
|
|
ident = elemAt identOrder index;
|
|
nextLevel = if index + 1 == length identOrder then getHandler else doLevel (index + 1);
|
|
in
|
|
list:
|
|
let
|
|
# Partition all matchers into ident-specific (.wrong) and -unspecific (.right) ones
|
|
parted = partition (m: isNull m.${ident}) list;
|
|
# We only use the unspecific ones to compute the fallback
|
|
fallback = nextLevel parted.right;
|
|
specific = pipe parted.wrong [
|
|
(groupBy (m: m.${ident}))
|
|
# For ident-specific handlers, the unspecific ones also apply
|
|
(mapAttrs (package: handlers: nextLevel (handlers ++ parted.right)))
|
|
# Memory optimisation: Don't need a specific handler if it would end up the same as the fallback
|
|
(filterAttrs (name: res: res != fallback))
|
|
];
|
|
in
|
|
# Optimisation in case it's always the same handler,
|
|
# can propagate up for the entire switch to just be a string
|
|
if specific == { } && isString fallback then
|
|
fallback
|
|
else
|
|
{
|
|
"${ident}Fallback" = fallback;
|
|
"${ident}Specific" = specific;
|
|
};
|
|
switch = doLevel 0 constraints;
|
|
in
|
|
{
|
|
inherit switch definedConstraints;
|
|
handlerForProblem =
|
|
if isString switch then
|
|
kind: name: pname:
|
|
switch
|
|
else
|
|
kind:
|
|
let
|
|
kindSwitch = switch.kindSpecific.${kind} or switch.kindFallback;
|
|
in
|
|
if isString kindSwitch then
|
|
name: pname: kindSwitch
|
|
else
|
|
name:
|
|
let
|
|
nameSwitch = kindSwitch.nameSpecific.${name} or kindSwitch.nameFallback;
|
|
in
|
|
if isString nameSwitch then
|
|
pname: nameSwitch
|
|
else
|
|
pname: nameSwitch.packageSpecific.${pname} or nameSwitch.packageFallback;
|
|
};
|
|
|
|
genCheckProblems =
|
|
config:
|
|
let
|
|
# This is here so that it gets cached for a (checkProblems config) thunk
|
|
inherit (genHandlerSwitch config)
|
|
handlerForProblem
|
|
definedConstraints
|
|
;
|
|
|
|
# All the problem kinds that actually need to be checked
|
|
configuredProblems = definedConstraints.kinds ++ definedConstraints.names;
|
|
|
|
# Filter out any problems that are always ignored in config.problems.
|
|
# Makes sure to cache the condition by appliny config, and the handler
|
|
# by applying the problem's kind and name
|
|
automaticProblemsConfigCache = concatMap (
|
|
problem:
|
|
optional (elem problem.kindName configuredProblems) {
|
|
condition = problem.condition config;
|
|
handler = handlerForProblem problem.kindName problem.kindName;
|
|
}
|
|
) automaticProblems;
|
|
in
|
|
attrs:
|
|
if
|
|
# Fast path for when there's no problem that needs to be handled
|
|
all (
|
|
problem: problem.condition attrs -> problem.handler (getName attrs) == "ignore"
|
|
) automaticProblemsConfigCache
|
|
&& (
|
|
# No manual problems
|
|
!attrs ? meta.problems
|
|
# Or all manual problems are ignored
|
|
|| all (
|
|
name: handlerForProblem (attrs.meta.problems.${name}.kind or name) name (getName attrs) == "ignore"
|
|
) (attrNames attrs.meta.problems)
|
|
)
|
|
then
|
|
null
|
|
else
|
|
# Slow path, only here we actually figure out which problems we need to handle
|
|
let
|
|
pname = getName attrs;
|
|
problems = attrs.meta.problems or { } // genAutomaticProblems config attrs;
|
|
problemsToHandle = filter (v: v.handler != "ignore") (
|
|
mapAttrsToList (name: problem: rec {
|
|
inherit name;
|
|
# Kind falls back to the name
|
|
kind = problem.kind or name;
|
|
handler = handlerForProblem kind name pname;
|
|
inherit problem;
|
|
}) problems
|
|
);
|
|
in
|
|
processProblems pname problemsToHandle;
|
|
|
|
completeMetaProblems =
|
|
config: attrs:
|
|
mapAttrs (name: problem: { kind = name; } // problem) (
|
|
(attrs.meta.problems or { }) // genAutomaticProblems config attrs
|
|
);
|
|
|
|
processProblems =
|
|
pname: problemsToHandle:
|
|
let
|
|
grouped = groupBy (v: v.handler) problemsToHandle;
|
|
|
|
warnProblems = grouped.warn or [ ];
|
|
errorProblems = grouped.error or [ ];
|
|
|
|
# assert annotatedProblems != [ ];
|
|
fullMessage =
|
|
v:
|
|
"${v.name}${optionalString (v.kind != v.name) " (kind \"${v.kind}\")"}: "
|
|
+ "${v.problem.message}${
|
|
optionalString (v.problem.urls or [ ] != [ ]) " (${concatStringsSep ", " v.problem.urls})"
|
|
}";
|
|
|
|
warnings = map (x: {
|
|
reason = "problem";
|
|
msg = "has the following problem: ${fullMessage x}";
|
|
remediation = "See https://nixos.org/manual/nixpkgs/unstable#sec-problems"; # TODO: Add remediation, maybe just link to docs to keep it small
|
|
}) warnProblems;
|
|
|
|
error =
|
|
if errorProblems == [ ] then
|
|
null
|
|
else
|
|
{
|
|
reason = "problem";
|
|
# Only there for PR CI evaluation
|
|
handleProblem =
|
|
handleEvalIssue:
|
|
lib.foldl' (result: problem: handleEvalIssue problem.kind (fullMessage problem)) true errorProblems;
|
|
msg = ''
|
|
has problems:
|
|
${concatMapStringsSep "\n" (x: "- ${fullMessage x}") errorProblems}
|
|
'';
|
|
## TODO: Add mention of problem.matchers, or maybe better link to docs of that
|
|
remediation = ''
|
|
See also https://nixos.org/manual/nixpkgs/unstable#sec-problems
|
|
To allow evaluation regardless, use:
|
|
- Nixpkgs import: import nixpkgs { config = <below code>; }
|
|
- NixOS: nixpkgs.config = <below code>;
|
|
- nix-* commands: Put below code in ~/.config/nixpkgs/config.nix
|
|
|
|
{
|
|
problems.handlers = {
|
|
${concatMapStringsSep "\n " (
|
|
problem:
|
|
''${escapeNixIdentifier pname}.${escapeNixIdentifier problem.name} = "warn"; # or "ignore"''
|
|
) errorProblems}
|
|
};
|
|
}
|
|
'';
|
|
};
|
|
in
|
|
{
|
|
inherit error warnings;
|
|
};
|
|
|
|
}
|