Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC 140 2a] pkgs/by-name: Enforce for new packages #275539

Merged
merged 6 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions pkgs/by-name/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
# Name-based package directories

The structure of this directory maps almost directly to top-level package attributes.
This is the recommended way to add new top-level packages to Nixpkgs [when possible](#limitations).
Add new top-level packages to Nixpkgs using this mechanism [whenever possible](#limitations).

Packages found in the named-based structure do not need to be explicitly added to the
`top-level/all-packages.nix` file unless they require overriding the default value
of an implicit attribute (see below).
Packages found in the name-based structure are automatically included, without needing to be added to `all-packages.nix`. However if the implicit attribute defaults need to be changed for a package, this [must still be declared in `all-packages.nix`](#changing-implicit-attribute-defaults).

## Example

Expand Down
2 changes: 2 additions & 0 deletions pkgs/test/nixpkgs-check-by-name/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ The current ratchets are:

- New manual definitions of `pkgs.${name}` (e.g. in `pkgs/top-level/all-packages.nix`) with `args = { }`
(see [nix evaluation checks](#nix-evaluation-checks)) must not be introduced.
- New top-level packages defined using `pkgs.callPackage` must be defined with a package directory.
- Once a top-level package uses `pkgs/by-name`, it also can't be moved back out of it.

## Development

Expand Down
4 changes: 4 additions & 0 deletions pkgs/test/nixpkgs-check-by-name/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
}:
let
runtimeExprPath = ./src/eval.nix;
nixpkgsLibPath = ../../../lib;
package =
rustPlatform.buildRustPackage {
name = "nixpkgs-check-by-name";
Expand All @@ -30,6 +31,8 @@ let
export NIX_STATE_DIR=$TEST_ROOT/var/nix
export NIX_STORE_DIR=$TEST_ROOT/store

export NIXPKGS_LIB_PATH=${nixpkgsLibPath}

# Ensure that even if tests run in parallel, we don't get an error
# We'd run into https://github.com/NixOS/nix/issues/2706 unless the store is initialised first
nix-store --init
Expand All @@ -44,6 +47,7 @@ let
'';
passthru.shell = mkShell {
env.NIX_CHECK_BY_NAME_EXPR_PATH = toString runtimeExprPath;
env.NIXPKGS_LIB_PATH = toString nixpkgsLibPath;
inputsFrom = [ package ];
};
};
Expand Down
36 changes: 29 additions & 7 deletions pkgs/test/nixpkgs-check-by-name/src/eval.nix
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,37 @@ let
};
};

attrInfos = map (name: [
name
(
byNameAttrs = builtins.listToAttrs (map (name: {
inherit name;
value.ByName =
if ! pkgs ? ${name} then
{ Missing = null; }
else
{ Existing = attrInfo name pkgs.${name}; }
)
]) attrs;
{ Existing = attrInfo name pkgs.${name}; };
}) attrs);

# Information on all attributes that exist but are not in pkgs/by-name.
# We need this to enforce pkgs/by-name for new packages
nonByNameAttrs = builtins.mapAttrs (name: value:
let
output = attrInfo name value;
result = builtins.tryEval (builtins.deepSeq output null);
in
{
NonByName =
if result.success then
{ EvalSuccess = output; }
else
{ EvalFailure = null; };
}
) (builtins.removeAttrs pkgs attrs);

# All attributes
attributes = byNameAttrs // nonByNameAttrs;
in
attrInfos
# We output them in the form [ [ <name> <value> ] ]` such that the Rust side
# doesn't need to sort them again to get deterministic behavior (good for testing)
map (name: [
name
attributes.${name}
]) (builtins.attrNames attributes)
99 changes: 89 additions & 10 deletions pkgs/test/nixpkgs-check-by-name/src/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use crate::nixpkgs_problem::NixpkgsProblem;
use crate::ratchet;
use crate::structure;
use crate::validation::{self, Validation::Success};
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::Path;

use anyhow::Context;
Expand All @@ -11,6 +13,21 @@ 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
Expand Down Expand Up @@ -56,7 +73,7 @@ enum CallPackageVariant {
pub fn check_values(
nixpkgs_path: &Path,
package_names: Vec<String>,
eval_accessible_paths: &[&Path],
eval_nix_path: &HashMap<String, PathBuf>,
) -> validation::Result<ratchet::Nixpkgs> {
// Write the list of packages we need to check into a temporary JSON file.
// This can then get read by the Nix evaluation.
Expand Down Expand Up @@ -105,9 +122,13 @@ pub fn check_values(
.arg(nixpkgs_path);

// Also add extra paths that need to be accessible
for path in eval_accessible_paths {
for (name, path) in eval_nix_path {
command.arg("-I");
command.arg(path);
let mut name_value = OsString::new();
name_value.push(name);
name_value.push("=");
name_value.push(path);
command.arg(name_value);
}
command.args(["-I", &expr_path]);
command.arg(expr_path);
Expand All @@ -120,7 +141,7 @@ pub fn check_values(
anyhow::bail!("Failed to run command {command:?}");
}
// Parse the resulting JSON value
let attributes: Vec<(String, ByNameAttribute)> = serde_json::from_slice(&result.stdout)
let attributes: Vec<(String, Attribute)> = serde_json::from_slice(&result.stdout)
.with_context(|| {
format!(
"Failed to deserialise {}",
Expand All @@ -133,30 +154,86 @@ pub fn check_values(
let relative_package_file = structure::relative_file_for_package(&attribute_name);

use ratchet::RatchetState::*;
use Attribute::*;
use AttributeInfo::*;
use ByNameAttribute::*;
use CallPackageVariant::*;
use NonByNameAttribute::*;

let check_result = match attribute_value {
Missing => NixpkgsProblem::UndefinedAttr {
// The attribute succeeds evaluation and is NOT defined in pkgs/by-name
NonByName(EvalSuccess(attribute_info)) => {
let uses_by_name = match attribute_info {
// In these cases the package doesn't qualify for being in pkgs/by-name,
// so the UsesByName ratchet is already as tight as it can be
NonAttributeSet => Success(Tight),
NonCallPackage => Success(Tight),
// This is an odd case when _internalCallByNamePackageFile is used to define a package.
CallPackage(CallPackageInfo {
call_package_variant: Auto,
..
}) => NixpkgsProblem::InternalCallPackageUsed {
attr_name: attribute_name.clone(),
}
.into(),
// Only derivations can be in pkgs/by-name,
// so this attribute doesn't qualify
CallPackage(CallPackageInfo {
is_derivation: false,
..
}) => Success(Tight),

// The case of an attribute that qualifies:
// - Uses callPackage
// - Is a derivation
CallPackage(CallPackageInfo {
is_derivation: true,
call_package_variant: Manual { path, empty_arg },
}) => Success(Loose(ratchet::UsesByName {
call_package_path: path,
empty_arg,
})),
};
uses_by_name.map(|x| ratchet::Package {
empty_non_auto_called: Tight,
uses_by_name: x,
})
}
NonByName(EvalFailure) => {
// This is a bit of an odd case: We don't even _know_ whether this attribute
// would qualify for using pkgs/by-name. We can either:
// - Assume it's not using pkgs/by-name, which has the problem that if a
// package evaluation gets broken temporarily, the fix can remove it from
// pkgs/by-name again
// - Assume it's using pkgs/by-name already, which has the problem that if a
// package evaluation gets broken temporarily, fixing it requires a move to
// pkgs/by-name
Comment on lines +203 to +210
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future ratchets might try to drive this number down, I might imagine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, I thought about implementing an eval-successful ratchet, but I think this should be a future PR. Then also it's also not pkgs/by-name-specific anymore and the tool could use a rename.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One only needs to look at the list of things detected by NixpkgsProblems to realize we've backed ourselves into a damn fine nixpkgs linter framework. 🚀

// We choose the latter, since we want to move towards pkgs/by-name, not away
// from it
Success(ratchet::Package {
empty_non_auto_called: Tight,
uses_by_name: Tight,
})
}
ByName(Missing) => NixpkgsProblem::UndefinedAttr {
relative_package_file: relative_package_file.clone(),
package_name: attribute_name.clone(),
}
.into(),
Existing(NonAttributeSet) => NixpkgsProblem::NonDerivation {
ByName(Existing(NonAttributeSet)) => NixpkgsProblem::NonDerivation {
relative_package_file: relative_package_file.clone(),
package_name: attribute_name.clone(),
}
.into(),
Existing(NonCallPackage) => NixpkgsProblem::WrongCallPackage {
ByName(Existing(NonCallPackage)) => NixpkgsProblem::WrongCallPackage {
relative_package_file: relative_package_file.clone(),
package_name: attribute_name.clone(),
}
.into(),
Existing(CallPackage(CallPackageInfo {
ByName(Existing(CallPackage(CallPackageInfo {
is_derivation,
call_package_variant,
})) => {
}))) => {
let check_result = if !is_derivation {
NixpkgsProblem::NonDerivation {
relative_package_file: relative_package_file.clone(),
Expand All @@ -170,6 +247,7 @@ pub fn check_values(
check_result.and(match &call_package_variant {
Auto => Success(ratchet::Package {
empty_non_auto_called: Tight,
uses_by_name: Tight,
}),
Manual { path, empty_arg } => {
let correct_file = if let Some(call_package_path) = path {
Expand All @@ -186,6 +264,7 @@ pub fn check_values(
} else {
Tight
},
uses_by_name: Tight,
})
} else {
NixpkgsProblem::WrongCallPackage {
Expand All @@ -203,7 +282,7 @@ pub fn check_values(
));

Ok(check_result.map(|elems| ratchet::Nixpkgs {
package_names,
package_names: elems.iter().map(|(name, _)| name.to_owned()).collect(),
infinisil marked this conversation as resolved.
Show resolved Hide resolved
package_map: elems.into_iter().collect(),
}))
}
36 changes: 28 additions & 8 deletions pkgs/test/nixpkgs-check-by-name/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::validation::Validation::Success;
use anyhow::Context;
use clap::Parser;
use colored::Colorize;
use std::collections::HashMap;
use std::io;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
Expand Down Expand Up @@ -44,7 +45,12 @@ pub struct Args {

fn main() -> ExitCode {
let args = Args::parse();
match process(&args.base, &args.nixpkgs, &[], &mut io::stderr()) {
match process(
&args.base,
&args.nixpkgs,
&HashMap::new(),
&mut io::stderr(),
) {
Ok(true) => {
eprintln!("{}", "Validated successfully".green());
ExitCode::SUCCESS
Expand Down Expand Up @@ -77,15 +83,15 @@ fn main() -> ExitCode {
pub fn process<W: io::Write>(
base_nixpkgs: &Path,
main_nixpkgs: &Path,
eval_accessible_paths: &[&Path],
eval_nix_path: &HashMap<String, PathBuf>,
error_writer: &mut W,
) -> anyhow::Result<bool> {
// Check the main Nixpkgs first
let main_result = check_nixpkgs(main_nixpkgs, eval_accessible_paths, error_writer)?;
let main_result = check_nixpkgs(main_nixpkgs, eval_nix_path, error_writer)?;
let check_result = main_result.result_map(|nixpkgs_version| {
// If the main Nixpkgs doesn't have any problems, run the ratchet checks against the base
// Nixpkgs
check_nixpkgs(base_nixpkgs, eval_accessible_paths, error_writer)?.result_map(
check_nixpkgs(base_nixpkgs, eval_nix_path, error_writer)?.result_map(
|base_nixpkgs_version| {
Ok(ratchet::Nixpkgs::compare(
base_nixpkgs_version,
Expand Down Expand Up @@ -113,7 +119,7 @@ pub fn process<W: io::Write>(
/// ratchet check against another result.
pub fn check_nixpkgs<W: io::Write>(
nixpkgs_path: &Path,
eval_accessible_paths: &[&Path],
eval_nix_path: &HashMap<String, PathBuf>,
error_writer: &mut W,
) -> validation::Result<ratchet::Nixpkgs> {
Ok({
Expand All @@ -134,7 +140,7 @@ pub fn check_nixpkgs<W: io::Write>(
} else {
check_structure(&nixpkgs_path)?.result_map(|package_names|
// Only if we could successfully parse the structure, we do the evaluation checks
eval::check_values(&nixpkgs_path, package_names, eval_accessible_paths))?
eval::check_values(&nixpkgs_path, package_names, eval_nix_path))?
}
})
}
Expand All @@ -144,8 +150,10 @@ mod tests {
use crate::process;
use crate::utils;
use anyhow::Context;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use tempfile::{tempdir_in, TempDir};

#[test]
Expand Down Expand Up @@ -226,7 +234,19 @@ mod tests {
}

fn test_nixpkgs(name: &str, path: &Path, expected_errors: &str) -> anyhow::Result<()> {
let extra_nix_path = Path::new("tests/mock-nixpkgs.nix");
let eval_nix_path = HashMap::from([
(
"test-nixpkgs".to_string(),
PathBuf::from("tests/mock-nixpkgs.nix"),
),
(
"test-nixpkgs/lib".to_string(),
PathBuf::from(
std::env::var("NIXPKGS_LIB_PATH")
.with_context(|| "Could not get environment variable NIXPKGS_LIB_PATH")?,
),
),
]);

let base_path = path.join("base");
let base_nixpkgs = if base_path.exists() {
Expand All @@ -238,7 +258,7 @@ mod tests {
// We don't want coloring to mess up the tests
let writer = temp_env::with_var("NO_COLOR", Some("1"), || -> anyhow::Result<_> {
let mut writer = vec![];
process(base_nixpkgs, &path, &[&extra_nix_path], &mut writer)
process(base_nixpkgs, &path, &eval_nix_path, &mut writer)
.with_context(|| format!("Failed test case {name}"))?;
Ok(writer)
})?;
Expand Down
Loading