use crate::nix_file::CallPackageArgumentInfo; use crate::nixpkgs_problem::NixpkgsProblem; use crate::ratchet; use crate::ratchet::RatchetState::Loose; use crate::ratchet::RatchetState::Tight; use crate::structure; use crate::utils; use crate::validation::ResultIteratorExt as _; use crate::validation::{self, Validation::Success}; use crate::NixFileStore; use relative_path::RelativePathBuf; use std::path::Path; use anyhow::Context; use serde::Deserialize; use std::path::PathBuf; use std::process; use tempfile::NamedTempFile; /// Attribute set of this structure is returned by eval.nix #[derive(Deserialize)] enum Attribute { /// An attribute that should be defined via pkgs/by-name ByName(ByNameAttribute), /// An attribute not defined via pkgs/by-name NonByName(NonByNameAttribute), } #[derive(Deserialize)] enum NonByNameAttribute { /// The attribute doesn't evaluate EvalFailure, EvalSuccess(AttributeInfo), } #[derive(Deserialize)] enum ByNameAttribute { /// The attribute doesn't exist at all Missing, Existing(AttributeInfo), } #[derive(Deserialize)] struct AttributeInfo { /// The location of the attribute as returned by `builtins.unsafeGetAttrPos` location: Option, attribute_variant: AttributeVariant, } /// The structure returned by a successful `builtins.unsafeGetAttrPos` #[derive(Deserialize, Clone, Debug)] struct Location { pub file: PathBuf, pub line: usize, pub column: usize, } impl Location { // Returns the [file] field, but relative to Nixpkgs fn relative_file(&self, nixpkgs_path: &Path) -> anyhow::Result { let path = self.file.strip_prefix(nixpkgs_path).with_context(|| { format!( "The file ({}) is outside Nixpkgs ({})", self.file.display(), nixpkgs_path.display() ) })?; Ok(RelativePathBuf::from_path(path).expect("relative path")) } } #[derive(Deserialize)] pub enum AttributeVariant { /// The attribute is not an attribute set, we're limited in the amount of information we can get /// from it (though it's obviously not a derivation) NonAttributeSet, AttributeSet { /// Whether the attribute is a derivation (`lib.isDerivation`) is_derivation: bool, /// The type of callPackage definition_variant: DefinitionVariant, }, } #[derive(Deserialize)] pub enum DefinitionVariant { /// An automatic definition by the `pkgs/by-name` overlay /// Though it's detected using the internal _internalCallByNamePackageFile attribute, /// which can in theory also be used by other code AutoDefinition, /// A manual definition of the attribute, typically in `all-packages.nix` ManualDefinition { /// Whether the attribute is defined as `pkgs.callPackage ...` or something else. is_semantic_call_package: bool, }, } /// Check that the Nixpkgs attribute values corresponding to the packages in pkgs/by-name are /// of the form `callPackage { ... }`. /// See the `eval.nix` file for how this is achieved on the Nix side pub fn check_values( nixpkgs_path: &Path, nix_file_store: &mut NixFileStore, package_names: Vec, keep_nix_path: bool, ) -> validation::Result { // Write the list of packages we need to check into a temporary JSON file. // This can then get read by the Nix evaluation. let attrs_file = NamedTempFile::new().with_context(|| "Failed to create a temporary file")?; // We need to canonicalise this path because if it's a symlink (which can be the case on // Darwin), Nix would need to read both the symlink and the target path, therefore need 2 // NIX_PATH entries for restrict-eval. But if we resolve the symlinks then only one predictable // entry is needed. let attrs_file_path = attrs_file.path().canonicalize()?; serde_json::to_writer(&attrs_file, &package_names).with_context(|| { format!( "Failed to serialise the package names to the temporary path {}", attrs_file_path.display() ) })?; let expr_path = std::env::var("NIX_CHECK_BY_NAME_EXPR_PATH") .with_context(|| "Could not get environment variable NIX_CHECK_BY_NAME_EXPR_PATH")?; // With restrict-eval, only paths in NIX_PATH can be accessed, so we explicitly specify the // ones needed needed let mut command = process::Command::new("nix-instantiate"); command // Inherit stderr so that error messages always get shown .stderr(process::Stdio::inherit()) .args([ "--eval", "--json", "--strict", "--readonly-mode", "--restrict-eval", "--show-trace", ]) // Pass the path to the attrs_file as an argument and add it to the NIX_PATH so it can be // accessed in restrict-eval mode .args(["--arg", "attrsPath"]) .arg(&attrs_file_path) .arg("-I") .arg(&attrs_file_path) // Same for the nixpkgs to test .args(["--arg", "nixpkgsPath"]) .arg(nixpkgs_path) .arg("-I") .arg(nixpkgs_path); // Clear NIX_PATH to be sure it doesn't influence the result // But not when requested to keep it, used so that the tests can pass extra Nix files if !keep_nix_path { command.env_remove("NIX_PATH"); } command.args(["-I", &expr_path]); command.arg(expr_path); let result = command .output() .with_context(|| format!("Failed to run command {command:?}"))?; if !result.status.success() { anyhow::bail!("Failed to run command {command:?}"); } // Parse the resulting JSON value let attributes: Vec<(String, Attribute)> = serde_json::from_slice(&result.stdout) .with_context(|| { format!( "Failed to deserialise {}", String::from_utf8_lossy(&result.stdout) ) })?; let check_result = validation::sequence( attributes .into_iter() .map(|(attribute_name, attribute_value)| { let check_result = match attribute_value { Attribute::NonByName(non_by_name_attribute) => handle_non_by_name_attribute( nixpkgs_path, nix_file_store, &attribute_name, non_by_name_attribute, )?, Attribute::ByName(by_name_attribute) => by_name( nix_file_store, nixpkgs_path, &attribute_name, by_name_attribute, )?, }; Ok::<_, anyhow::Error>(check_result.map(|value| (attribute_name.clone(), value))) }) .collect_vec()?, ); Ok(check_result.map(|elems| ratchet::Nixpkgs { package_names: elems.iter().map(|(name, _)| name.to_owned()).collect(), package_map: elems.into_iter().collect(), })) } /// Handles the evaluation result for an attribute in `pkgs/by-name`, /// turning it into a validation result. fn by_name( nix_file_store: &mut NixFileStore, nixpkgs_path: &Path, attribute_name: &str, by_name_attribute: ByNameAttribute, ) -> validation::Result { use ratchet::RatchetState::*; use ByNameAttribute::*; let relative_package_file = structure::relative_file_for_package(attribute_name); // At this point we know that `pkgs/by-name/fo/foo/package.nix` has to exists. // This match decides whether the attribute `foo` is defined accordingly // and whether a legacy manual definition could be removed let manual_definition_result = match by_name_attribute { // The attribute is missing Missing => { // This indicates a bug in the `pkgs/by-name` overlay, because it's supposed to // automatically defined attributes in `pkgs/by-name` NixpkgsProblem::UndefinedAttr { relative_package_file: relative_package_file.to_owned(), package_name: attribute_name.to_owned(), } .into() } // The attribute exists Existing(AttributeInfo { // But it's not an attribute set, which limits the amount of information we can get // about this attribute (see ./eval.nix) attribute_variant: AttributeVariant::NonAttributeSet, location: _location, }) => { // The only thing we know is that it's definitely not a derivation, since those are // always attribute sets. // // We can't know whether the attribute is automatically or manually defined for sure, // and while we could check the location, the error seems clear enough as is. NixpkgsProblem::NonDerivation { relative_package_file: relative_package_file.to_owned(), package_name: attribute_name.to_owned(), } .into() } // The attribute exists Existing(AttributeInfo { // And it's an attribute set, which allows us to get more information about it attribute_variant: AttributeVariant::AttributeSet { is_derivation, definition_variant, }, location, }) => { // Only derivations are allowed in `pkgs/by-name` let is_derivation_result = if is_derivation { Success(()) } else { NixpkgsProblem::NonDerivation { relative_package_file: relative_package_file.to_owned(), package_name: attribute_name.to_owned(), } .into() }; // If the definition looks correct let variant_result = match definition_variant { // An automatic `callPackage` by the `pkgs/by-name` overlay. // Though this gets detected by checking whether the internal // `_internalCallByNamePackageFile` was used DefinitionVariant::AutoDefinition => { if let Some(_location) = location { // Such an automatic definition should definitely not have a location // Having one indicates that somebody is using `_internalCallByNamePackageFile`, NixpkgsProblem::InternalCallPackageUsed { attr_name: attribute_name.to_owned(), } .into() } else { Success(Tight) } } // The attribute is manually defined, e.g. in `all-packages.nix`. // This means we need to enforce it to look like this: // callPackage ../pkgs/by-name/fo/foo/package.nix { ... } DefinitionVariant::ManualDefinition { is_semantic_call_package, } => { // We should expect manual definitions to have a location, otherwise we can't // enforce the expected format if let Some(location) = location { // Parse the Nix file in the location let nix_file = nix_file_store.get(&location.file)?; // The relative path of the Nix file, for error messages let relative_location_file = location.relative_file(nixpkgs_path).with_context(|| { format!("Failed to resolve the file where attribute {attribute_name} is defined") })?; // Figure out whether it's an attribute definition of the form `= callPackage `, // returning the arguments if so. let (optional_syntactic_call_package, definition) = nix_file .call_package_argument_info_at(location.line, location.column, nixpkgs_path) .with_context(|| { format!("Failed to get the definition info for attribute {attribute_name}") })?; by_name_override( attribute_name, relative_package_file, is_semantic_call_package, optional_syntactic_call_package, definition, location, relative_location_file, ) } else { // If manual definitions don't have a location, it's likely `mapAttrs`'d // over, e.g. if it's defined in aliases.nix. // We can't verify whether its of the expected `callPackage`, so error out NixpkgsProblem::CannotDetermineAttributeLocation { attr_name: attribute_name.to_owned(), } .into() } } }; // Independently report problems about whether it's a derivation and the callPackage variant is_derivation_result.and(variant_result) } }; Ok( // Packages being checked in this function are _always_ already defined in `pkgs/by-name`, // so instead of repeating ourselves all the time to define `uses_by_name`, just set it // once at the end with a map manual_definition_result.map(|manual_definition| ratchet::Package { manual_definition, uses_by_name: Tight, }), ) } /// Handles the case for packages in `pkgs/by-name` that are manually overridden, e.g. in /// all-packages.nix fn by_name_override( attribute_name: &str, expected_package_file: RelativePathBuf, is_semantic_call_package: bool, optional_syntactic_call_package: Option, definition: String, location: Location, relative_location_file: RelativePathBuf, ) -> validation::Validation> { // At this point, we completed two different checks for whether it's a // `callPackage` match (is_semantic_call_package, optional_syntactic_call_package) { // Something like ` = foo` (_, None) => NixpkgsProblem::NonSyntacticCallPackage { package_name: attribute_name.to_owned(), file: relative_location_file, line: location.line, column: location.column, definition, } .into(), // Something like ` = pythonPackages.callPackage ...` (false, Some(_)) => NixpkgsProblem::NonToplevelCallPackage { package_name: attribute_name.to_owned(), file: relative_location_file, line: location.line, column: location.column, definition, } .into(), // Something like ` = pkgs.callPackage ...` (true, Some(syntactic_call_package)) => { if let Some(actual_package_file) = syntactic_call_package.relative_path { if actual_package_file != expected_package_file { // Wrong path NixpkgsProblem::WrongCallPackagePath { package_name: attribute_name.to_owned(), file: relative_location_file, line: location.line, actual_path: actual_package_file, expected_path: expected_package_file, } .into() } else { // Manual definitions with empty arguments are not allowed // anymore, but existing ones should continue to be allowed let manual_definition_ratchet = if syntactic_call_package.empty_arg { // This is the state to migrate away from Loose(NixpkgsProblem::EmptyArgument { package_name: attribute_name.to_owned(), file: relative_location_file, line: location.line, column: location.column, definition, }) } else { // This is the state to migrate to Tight }; Success(manual_definition_ratchet) } } else { // No path NixpkgsProblem::NonPath { package_name: attribute_name.to_owned(), file: relative_location_file, line: location.line, column: location.column, definition, } .into() } } } } /// Handles the evaluation result for an attribute _not_ in `pkgs/by-name`, /// turning it into a validation result. fn handle_non_by_name_attribute( nixpkgs_path: &Path, nix_file_store: &mut NixFileStore, attribute_name: &str, non_by_name_attribute: NonByNameAttribute, ) -> validation::Result { use ratchet::RatchetState::*; use NonByNameAttribute::*; // The ratchet state whether this attribute uses `pkgs/by-name`. // This is never `Tight`, because we only either: // - Know that the attribute _could_ be migrated to `pkgs/by-name`, which is `Loose` // - Or we're unsure, in which case we use NonApplicable let uses_by_name = // This is a big ol' match on various properties of the attribute // First, it needs to succeed evaluation. We can't know whether an attribute could be // migrated to `pkgs/by-name` if it doesn't evaluate, since we need to check that it's a // derivation. // // This only has the minor negative effect that if a PR that breaks evaluation // gets merged, fixing those failures won't force anything into `pkgs/by-name`. // // For now this isn't our problem, but in the future we // might have another check to enforce that evaluation must not be broken. // // The alternative of assuming that failing attributes would have been fit for `pkgs/by-name` // has the problem that if a package evaluation gets broken temporarily, // fixing it requires a move to pkgs/by-name, which could happen more // often and isn't really justified. if let EvalSuccess(AttributeInfo { // We're only interested in attributes that are attribute sets (which includes // derivations). Anything else can't be in `pkgs/by-name`. attribute_variant: AttributeVariant::AttributeSet { // Indeed, we only care about derivations, non-derivation attribute sets can't be // in `pkgs/by-name` is_derivation: true, // Of the two definition variants, really only the manual one makes sense here. // Special cases are: // - Manual aliases to auto-called packages are not treated as manual definitions, // due to limitations in the semantic callPackage detection. So those should be // ignored. // - Manual definitions using the internal _internalCallByNamePackageFile are // not treated as manual definitions, since _internalCallByNamePackageFile is // used to detect automatic ones. We can't distinguish from the above case, so we // just need to ignore this one too, even if that internal attribute should never // be called manually. definition_variant: DefinitionVariant::ManualDefinition { is_semantic_call_package } }, // We need the location of the manual definition, because otherwise // we can't figure out whether it's a syntactic callPackage location: Some(location), }) = non_by_name_attribute { // Parse the Nix file in the location let nix_file = nix_file_store.get(&location.file)?; // The relative path of the Nix file, for error messages let relative_location_file = location.relative_file(nixpkgs_path).with_context(|| { format!("Failed to resolve the file where attribute {attribute_name} is defined") })?; // Figure out whether it's an attribute definition of the form `= callPackage `, // returning the arguments if so. let (optional_syntactic_call_package, _definition) = nix_file .call_package_argument_info_at( location.line, location.column, // Passing the Nixpkgs path here both checks that the is within Nixpkgs, and // strips the absolute Nixpkgs path from it, such that // syntactic_call_package.relative_path is relative to Nixpkgs nixpkgs_path ) .with_context(|| { format!("Failed to get the definition info for attribute {attribute_name}") })?; // At this point, we completed two different checks for whether it's a // `callPackage` match (is_semantic_call_package, optional_syntactic_call_package) { // Something like ` = { }` (false, None) // Something like ` = pythonPackages.callPackage ...` | (false, Some(_)) // Something like ` = bar` where `bar = pkgs.callPackage ...` | (true, None) => { // In all of these cases, it's not possible to migrate the package to `pkgs/by-name` NonApplicable } // Something like ` = pkgs.callPackage ...` (true, Some(syntactic_call_package)) => { // It's only possible to migrate such a definitions if.. match syntactic_call_package.relative_path { Some(ref rel_path) if rel_path.starts_with(utils::BASE_SUBPATH) => { // ..the path is not already within `pkgs/by-name` like // // foo-variant = callPackage ../by-name/fo/foo/package.nix { // someFlag = true; // } // // While such definitions could be moved to `pkgs/by-name` by using // `.override { someFlag = true; }` instead, this changes the semantics in // relation with overlays, so migration is generally not possible. // // See also "package variants" in RFC 140: // https://github.com/NixOS/rfcs/blob/master/rfcs/0140-simple-package-paths.md#package-variants NonApplicable } _ => { // Otherwise, the path is outside `pkgs/by-name`, which means it can be // migrated Loose((syntactic_call_package, relative_location_file)) } } } } } else { // This catches all the cases not matched by the above `if let`, falling back to not being // able to migrate such attributes NonApplicable }; Ok(Success(ratchet::Package { // Packages being checked in this function _always_ need a manual definition, because // they're not using `pkgs/by-name` which would allow avoiding it. // so instead of repeating ourselves all the time to define `manual_definition`, // just set it once at the end here manual_definition: Tight, uses_by_name, })) }