core/pkgs/stdenv/generic/check-meta.nix

539 lines
18 KiB
Nix
Raw Normal View History

2024-05-02 00:46:19 +00:00
# Checks derivation meta and attrs for problems (like brokenness,
# licenses, etc).
{ lib, config, hostPlatform }:
let
inherit (lib)
2024-05-13 21:24:10 +00:00
all attrNames concatMapStrings concatMapStringsSep concatStrings findFirst
isDerivation length concatMap mutuallyExclusive optional optionalAttrs
optionalString optionals isAttrs isString mapAttrs;
inherit (lib.lists) any toList isList elem;
inherit (lib.meta) availableOn;
inherit (lib.generators) toPretty;
2024-05-02 00:46:19 +00:00
# If we're in hydra, we can dispense with the more verbose error
# messages and make problems easier to spot.
inHydra = config.inHydra or false;
# Allow the user to opt-into additional warnings, e.g.
# import <nixpkgs> { config = { showDerivationWarnings = [ "maintainerless" ]; }; }
showWarnings = config.showDerivationWarnings;
2024-05-13 21:24:10 +00:00
getName = attrs:
attrs.name or ("${attrs.pname or "«name-missing»"}-${
attrs.version or "«version-missing»"
}");
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
allowUnfree = config.allowUnfree || builtins.getEnv "NIXPKGS_ALLOW_UNFREE"
== "1";
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
allowNonSource = let envVar = builtins.getEnv "NIXPKGS_ALLOW_NONSOURCE";
in if envVar != "" then envVar != "0" else config.allowNonSource or true;
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
allowlist = config.allowlistedLicenses or config.whitelistedLicenses or [ ];
blocklist = config.blocklistedLicenses or config.blacklistedLicenses or [ ];
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
areLicenseListsValid = if mutuallyExclusive allowlist blocklist then
true
else
throw
"allowlistedLicenses and blocklistedLicenses are not mutually exclusive.";
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
hasLicense = attrs: attrs ? meta.license;
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
hasListedLicense = assert areLicenseListsValid;
list: attrs:
length list > 0 && hasLicense attrs && (if isList attrs.meta.license then
any (l: elem l list) attrs.meta.license
else
elem attrs.meta.license list);
2024-05-02 00:46:19 +00:00
hasAllowlistedLicense = attrs: hasListedLicense allowlist attrs;
hasBlocklistedLicense = attrs: hasListedLicense blocklist attrs;
2024-05-13 21:24:10 +00:00
allowBroken = config.allowBroken || builtins.getEnv "NIXPKGS_ALLOW_BROKEN"
== "1";
2024-05-02 00:46:19 +00:00
allowUnsupportedSystem = config.allowUnsupportedSystem
|| builtins.getEnv "NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM" == "1";
isUnfree = licenses:
2024-05-13 21:24:10 +00:00
if isAttrs licenses then
!licenses.free or true
# TODO: Returning false in the case of a string is a bug that should be fixed.
# In a previous implementation of this function the function body
# was `licenses: lib.lists.any (l: !l.free or true) licenses;`
# which always evaluates to `!true` for strings.
else if isString licenses then
false
else
any (l: !l.free or true) licenses;
2024-05-02 00:46:19 +00:00
hasUnfreeLicense = attrs: hasLicense attrs && isUnfree attrs.meta.license;
hasNoMaintainers = attrs:
attrs ? meta.maintainers && (length attrs.meta.maintainers) == 0;
isMarkedBroken = attrs: attrs.meta.broken or false;
2024-05-13 21:24:10 +00:00
hasUnsupportedPlatform = pkg: !(availableOn hostPlatform pkg);
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
isMarkedInsecure = attrs: (attrs.meta.knownVulnerabilities or [ ]) != [ ];
2024-05-02 00:46:19 +00:00
# Alow granular checks to allow only some unfree packages
# Example:
# {pkgs, ...}:
# {
# allowUnfree = false;
# allowUnfreePredicate = (x: pkgs.lib.hasPrefix "vscode" x.name);
# }
allowUnfreePredicate = config.allowUnfreePredicate or (x: false);
# Check whether unfree packages are allowed and if not, whether the
# package has an unfree license and is not explicitly allowed by the
# `allowUnfreePredicate` function.
hasDeniedUnfreeLicense = attrs:
2024-05-13 21:24:10 +00:00
hasUnfreeLicense attrs && !allowUnfree && !allowUnfreePredicate attrs;
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
allowInsecureDefaultPredicate = x:
builtins.elem (getName x) (config.permittedInsecurePackages or [ ]);
allowInsecurePredicate = x:
(config.allowInsecurePredicate or allowInsecureDefaultPredicate) x;
2024-05-02 00:46:19 +00:00
hasAllowedInsecure = attrs:
2024-05-13 21:24:10 +00:00
!(isMarkedInsecure attrs) || allowInsecurePredicate attrs
|| builtins.getEnv "NIXPKGS_ALLOW_INSECURE" == "1";
2024-05-02 00:46:19 +00:00
isNonSource = sourceTypes: any (t: !t.isSource) sourceTypes;
hasNonSourceProvenance = attrs:
2024-05-13 21:24:10 +00:00
(attrs ? meta.sourceProvenance) && isNonSource attrs.meta.sourceProvenance;
2024-05-02 00:46:19 +00:00
# Allow granular checks to allow only some non-source-built packages
# Example:
# { pkgs, ... }:
# {
# allowNonSource = false;
# allowNonSourcePredicate = with pkgs.lib.lists; pkg: !(any (p: !p.isSource && p != lib.sourceTypes.binaryFirmware) pkg.meta.sourceProvenance);
# }
allowNonSourcePredicate = config.allowNonSourcePredicate or (x: false);
# Check whether non-source packages are allowed and if not, whether the
# package has non-source provenance and is not explicitly allowed by the
# `allowNonSourcePredicate` function.
hasDeniedNonSourceProvenance = attrs:
2024-05-13 21:24:10 +00:00
hasNonSourceProvenance attrs && !allowNonSource
&& !allowNonSourcePredicate attrs;
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
showLicenseOrSourceType = value:
toString (map (v: v.shortName or "unknown") (toList value));
2024-05-02 00:46:19 +00:00
showLicense = showLicenseOrSourceType;
showSourceType = showLicenseOrSourceType;
pos_str = meta: meta.position or "«unknown-file»";
remediation = {
2024-05-13 21:24:10 +00:00
unfree =
remediate_allowlist "Unfree" (remediate_predicate "allowUnfreePredicate");
non-source = remediate_allowlist "NonSource"
(remediate_predicate "allowNonSourcePredicate");
2024-05-02 00:46:19 +00:00
broken = remediate_allowlist "Broken" (x: "");
unsupported = remediate_allowlist "UnsupportedSystem" (x: "");
blocklisted = x: "";
insecure = remediate_insecure;
broken-outputs = remediateOutputsToInstall;
unknown-meta = x: "";
maintainerless = x: "";
};
2024-05-13 21:24:10 +00:00
remediation_env_var = allow_attr:
{
Unfree = "NIXPKGS_ALLOW_UNFREE";
Broken = "NIXPKGS_ALLOW_BROKEN";
UnsupportedSystem = "NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM";
NonSource = "NIXPKGS_ALLOW_NONSOURCE";
}.${allow_attr};
remediation_phrase = allow_attr:
{
Unfree = "unfree packages";
Broken = "broken packages";
UnsupportedSystem = "packages that are unsupported for this system";
NonSource = "packages not built from source";
}.${allow_attr};
remediate_predicate = predicateConfigAttr: attrs: ''
Alternatively you can configure a predicate to allow specific packages:
{ nixpkgs.config.${predicateConfigAttr} = pkg: builtins.elem (lib.getName pkg) [
"${lib.getName attrs}"
];
}
'';
# flakeNote will be printed in the remediation messages below.
flakeNote =
"\n Note: When using `nix shell`, `nix build`, `nix develop`, etc with a flake,\n then pass `--impure` in order to allow use of environment variables.\n ";
remediate_allowlist = allow_attr: rebuild_amendment: attrs: ''
a) To temporarily allow ${
remediation_phrase allow_attr
}, you can use an environment variable
for a single invocation of the nix tools.
$ export ${remediation_env_var allow_attr}=1
${flakeNote}
b) For `nixos-rebuild` you can set
{ nixpkgs.config.allow${allow_attr} = true; }
in configuration.nix to override this.
${rebuild_amendment attrs}
c) For `nix-env`, `nix-build`, `nix-shell` or any other Nix command you can add
{ allow${allow_attr} = true; }
to ~/.config/nixpkgs/config.nix.
'';
2024-05-02 00:46:19 +00:00
remediate_insecure = attrs:
''
Known issues:
2024-05-13 21:24:10 +00:00
'' + (concatStrings
(map (issue: " - ${issue}\n") attrs.meta.knownVulnerabilities)) + ''
2024-05-02 00:46:19 +00:00
You can install it anyway by allowing this package, using the
following methods:
a) To temporarily allow all insecure packages, you can use an environment
variable for a single invocation of the nix tools:
$ export NIXPKGS_ALLOW_INSECURE=1
${flakeNote}
b) for `nixos-rebuild` you can add ${getName attrs} to
`nixpkgs.config.permittedInsecurePackages` in the configuration.nix,
like so:
{
nixpkgs.config.permittedInsecurePackages = [
"${getName attrs}"
];
}
c) For `nix-env`, `nix-build`, `nix-shell` or any other Nix command you can add
${getName attrs} to `permittedInsecurePackages` in
~/.config/nixpkgs/config.nix, like so:
{
permittedInsecurePackages = [
"${getName attrs}"
];
}
'';
2024-05-13 21:24:10 +00:00
remediateOutputsToInstall = attrs:
let
expectedOutputs = attrs.meta.outputsToInstall or [ ];
2024-05-02 00:46:19 +00:00
actualOutputs = attrs.outputs or [ "out" ];
2024-05-13 21:24:10 +00:00
missingOutputs =
builtins.filter (output: !builtins.elem output actualOutputs)
expectedOutputs;
2024-05-02 00:46:19 +00:00
in ''
2024-05-13 21:24:10 +00:00
The package ${getName attrs} has set meta.outputsToInstall to: ${
builtins.concatStringsSep ", " expectedOutputs
}
2024-05-02 00:46:19 +00:00
2024-05-13 21:24:10 +00:00
however ${getName attrs} only has the outputs: ${
builtins.concatStringsSep ", " actualOutputs
}
2024-05-02 00:46:19 +00:00
and is missing the following ouputs:
${concatStrings (builtins.map (output: " - ${output}\n") missingOutputs)}
'';
2024-05-13 21:24:10 +00:00
handleEvalIssue = { meta, attrs }:
{ reason, errormsg ? "" }:
2024-05-02 00:46:19 +00:00
let
2024-05-13 21:24:10 +00:00
msg = if inHydra then
"Failed to evaluate ${getName attrs}: «${reason}»: ${errormsg}"
else
''
Package ${getName attrs} in ${
pos_str meta
} ${errormsg}, refusing to evaluate.
2024-05-02 00:46:19 +00:00
'' + (builtins.getAttr reason remediation) attrs;
2024-05-13 21:24:10 +00:00
handler = if config ? handleEvalIssue then
config.handleEvalIssue reason
else
throw;
2024-05-02 00:46:19 +00:00
in handler msg;
2024-05-13 21:24:10 +00:00
handleEvalWarning = { meta, attrs }:
{ reason, errormsg ? "" }:
2024-05-02 00:46:19 +00:00
let
remediationMsg = (builtins.getAttr reason remediation) attrs;
2024-05-13 21:24:10 +00:00
msg = if inHydra then
"Warning while evaluating ${getName attrs}: «${reason}»: ${errormsg}"
else
"Package ${getName attrs} in ${
pos_str meta
} ${errormsg}, continuing anyway."
+ (optionalString (remediationMsg != "") ''
${remediationMsg}'');
2024-05-02 00:46:19 +00:00
isEnabled = findFirst (x: x == reason) null showWarnings;
in if isEnabled != null then builtins.trace msg true else true;
metaTypes = let
types = import ./meta-types.nix { inherit lib; };
inherit (types) str union int attrs attrsOf any listOf bool;
2024-05-13 21:24:10 +00:00
platforms =
listOf (union [ str (attrsOf any) ]); # see lib.meta.platformMatch
2024-05-02 00:46:19 +00:00
in {
# These keys are documented
description = str;
mainProgram = str;
longDescription = str;
branch = str;
2024-05-13 21:24:10 +00:00
homepage = union [ (listOf str) str ];
2024-05-02 00:46:19 +00:00
downloadPage = str;
2024-05-13 21:24:10 +00:00
changelog = union [ (listOf str) str ];
2024-05-02 00:46:19 +00:00
license = let
# TODO disallow `str` licenses, use a module
2024-05-13 21:24:10 +00:00
licenseType = union [ (attrsOf any) str ];
in union [ (listOf licenseType) licenseType ];
2024-05-02 00:46:19 +00:00
sourceProvenance = listOf attrs;
2024-05-13 21:24:10 +00:00
maintainers = listOf (attrsOf
any); # TODO use the maintainer type from lib/tests/maintainer-module.nix
2024-05-02 00:46:19 +00:00
priority = int;
pkgConfigModules = listOf str;
inherit platforms;
hydraPlatforms = listOf str;
broken = bool;
unfree = bool;
unsupported = bool;
insecure = bool;
tests = {
name = "test";
2024-05-13 21:24:10 +00:00
verify = x:
x == { } || ( # Accept {} for tests that are unsupported
isDerivation x && x ? meta.timeout);
2024-05-02 00:46:19 +00:00
};
timeout = int;
# Needed for Hydra to expose channel tarballs:
# https://github.com/NixOS/hydra/blob/53335323ae79ca1a42643f58e520b376898ce641/doc/manual/src/jobs.md#meta-fields
isHydraChannel = bool;
# Weirder stuff that doesn't appear in the documentation?
maxSilent = int;
knownVulnerabilities = listOf str;
name = str;
version = str;
tag = str;
executables = listOf str;
outputsToInstall = listOf str;
position = str;
available = any;
isBuildPythonPackage = platforms;
schedulingPriority = int;
isFcitxEngine = bool;
isIbusEngine = bool;
isGutenprint = bool;
badPlatforms = platforms;
};
checkMetaAttr = let
# Map attrs directly to the verify function for performance
metaTypes' = mapAttrs (_: t: t.verify) metaTypes;
in k: v:
2024-05-13 21:24:10 +00:00
if metaTypes ? ${k} then
if metaTypes'.${k} v then
[ ]
else [''
key 'meta.${k}' has invalid value; expected ${metaTypes.${k}.name}, got
${toPretty { indent = " "; } v}'']
else [''
key 'meta.${k}' is unrecognized; expected one of:
[${concatMapStringsSep ", " (x: "'${x}'") (attrNames metaTypes)}]''];
checkMeta = meta:
optionals config.checkMeta
(concatMap (attr: checkMetaAttr attr meta.${attr}) (attrNames meta));
checkOutputsToInstall = attrs:
let
expectedOutputs = attrs.meta.outputsToInstall or [ ];
2024-05-02 00:46:19 +00:00
actualOutputs = attrs.outputs or [ "out" ];
2024-05-13 21:24:10 +00:00
missingOutputs =
builtins.filter (output: !builtins.elem output actualOutputs)
expectedOutputs;
in if config.checkMeta then builtins.length missingOutputs > 0 else false;
2024-05-02 00:46:19 +00:00
# Check if a derivation is valid, that is whether it passes checks for
# e.g brokenness or license.
#
# Return { valid: "yes", "warn" or "no" } and additionally
# { reason: String; errormsg: String } if it is not valid, where
# reason is one of "unfree", "blocklisted", "broken", "insecure", ...
# !!! reason strings are hardcoded into OfBorg, make sure to keep them in sync
# Along with a boolean flag for each reason
2024-05-13 21:24:10 +00:00
checkValidity = let
validYes = {
valid = "yes";
handled = true;
};
in attrs:
# Check meta attribute types first, to make sure it is always called even when there are other issues
# Note that this is not a full type check and functions below still need to by careful about their inputs!
let res = checkMeta (attrs.meta or { });
in if res != [ ] then {
valid = "no";
reason = "unknown-meta";
errormsg = ''
has an invalid meta attrset:${concatMapStrings (x: "\n - " + x) res}
'';
}
# --- Put checks that cannot be ignored here ---
else if checkOutputsToInstall attrs then {
valid = "no";
reason = "broken-outputs";
errormsg = "has invalid meta.outputsToInstall";
}
# --- Put checks that can be ignored here ---
else if hasDeniedUnfreeLicense attrs && !(hasAllowlistedLicense attrs) then {
valid = "no";
reason = "unfree";
errormsg = "has an unfree license (${showLicense attrs.meta.license})";
} else if hasBlocklistedLicense attrs then {
valid = "no";
reason = "blocklisted";
errormsg =
"has a blocklisted license (${showLicense attrs.meta.license})";
} else if hasDeniedNonSourceProvenance attrs then {
valid = "no";
reason = "non-source";
errormsg = "contains elements not built from source (${
showSourceType attrs.meta.sourceProvenance
})";
} else if !allowBroken && attrs.meta.broken or false then {
valid = "no";
reason = "broken";
errormsg = "is marked as broken";
} else if !allowUnsupportedSystem && hasUnsupportedPlatform attrs then
2024-05-02 00:46:19 +00:00
let
2024-05-13 21:24:10 +00:00
toPretty' = toPretty {
allowPrettyValues = true;
indent = " ";
2024-05-02 00:46:19 +00:00
};
2024-05-13 21:24:10 +00:00
in {
valid = "no";
reason = "unsupported";
errormsg = ''
is not available on the requested hostPlatform:
hostPlatform.config = "${hostPlatform.config}"
package.meta.platforms = ${toPretty' (attrs.meta.platforms or [ ])}
package.meta.badPlatforms = ${
toPretty' (attrs.meta.badPlatforms or [ ])
}
'';
}
else if !(hasAllowedInsecure attrs) then {
valid = "no";
reason = "insecure";
errormsg = "is marked as insecure";
}
# --- warnings ---
# Please also update the type in /pkgs/top-level/config.nix alongside this.
else if hasNoMaintainers attrs then {
valid = "warn";
reason = "maintainerless";
errormsg = "has no maintainers";
}
# -----
else
validYes;
2024-05-02 00:46:19 +00:00
# The meta attribute is passed in the resulting attribute set,
# but it's not part of the actual derivation, i.e., it's not
# passed to the builder and is not a dependency. But since we
# include it in the result, it *is* available to nix-env for queries.
# Example:
# meta = checkMeta.commonMeta { inherit validity attrs pos references; };
# validity = checkMeta.assertValidity { inherit meta attrs; };
commonMeta = { validity, attrs, pos ? null, references ? [ ] }:
let
outputs = attrs.outputs or [ "out" ];
hasOutput = out: builtins.elem out outputs;
2024-05-13 21:24:10 +00:00
in {
2024-05-02 00:46:19 +00:00
# `name` derivation attribute includes cross-compilation cruft,
# is under assert, and is sanitized.
# Let's have a clean always accessible version here.
name = attrs.name or "${attrs.pname}-${attrs.version}";
# If the packager hasn't specified `outputsToInstall`, choose a default,
# which is the name of `p.bin or p.out or p` along with `p.man` when
# present.
#
# If the packager has specified it, it will be overridden below in
# `// meta`.
#
# Note: This default probably shouldn't be globally configurable.
# Services and users should specify outputs explicitly,
# unless they are comfortable with this default.
2024-05-13 21:24:10 +00:00
outputsToInstall = [
(if hasOutput "bin" then
"bin"
else if hasOutput "out" then
"out"
else
findFirst hasOutput null outputs)
] ++ optional (hasOutput "man") "man";
} // attrs.meta or { }
2024-05-02 00:46:19 +00:00
# Fill `meta.position` to identify the source location of the package.
// optionalAttrs (pos != null) {
position = pos.file + ":" + toString pos.line;
} // {
# Expose the result of the checks for everyone to see.
unfree = hasUnfreeLicense attrs;
broken = isMarkedBroken attrs;
unsupported = hasUnsupportedPlatform attrs;
insecure = isMarkedInsecure attrs;
available = validity.valid != "no"
2024-05-13 21:24:10 +00:00
&& (if config.checkMetaRecursively or false then
all (d: d.meta.available or true) references
else
true);
2024-05-02 00:46:19 +00:00
};
2024-05-13 21:24:10 +00:00
assertValidity = { meta, attrs }:
let
2024-05-02 00:46:19 +00:00
validity = checkValidity attrs;
inherit (validity) valid;
2024-05-13 21:24:10 +00:00
in if validity ? handled then
validity
else
validity // {
# Throw an error if trying to evaluate a non-valid derivation
# or, alternatively, just output a warning message.
handled = (if valid == "yes" then
true
else if valid == "no" then
(handleEvalIssue { inherit meta attrs; } {
inherit (validity) reason errormsg;
})
else if valid == "warn" then
(handleEvalWarning { inherit meta attrs; } {
inherit (validity) reason errormsg;
})
else
throw "Unknown validitiy: '${valid}'");
};
2024-05-02 00:46:19 +00:00
in { inherit assertValidity commonMeta; }