about summary refs log tree commit diff
path: root/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/eval.rs
blob: cd8c70472cf25752fe1e559607dd627f7acaff10 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
use crate::nixpkgs_problem::NixpkgsProblem;
use crate::ratchet;
use crate::structure;
use crate::validation::{self, Validation::Success};
use std::path::Path;

use anyhow::Context;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::PathBuf;
use std::process;
use tempfile::NamedTempFile;

/// Attribute set of this structure is returned by eval.nix
#[derive(Deserialize)]
struct AttributeInfo {
    variant: AttributeVariant,
    is_derivation: bool,
}

#[derive(Deserialize)]
enum AttributeVariant {
    /// The attribute is auto-called as pkgs.callPackage using pkgs/by-name,
    /// and it is not overridden by a definition in all-packages.nix
    AutoCalled,
    /// The attribute is defined as a pkgs.callPackage <path> <args>,
    /// and overridden by all-packages.nix
    CallPackage {
        /// The <path> argument or None if it's not a path
        path: Option<PathBuf>,
        /// true if <args> is { }
        empty_arg: bool,
    },
    /// The attribute is not defined as pkgs.callPackage
    Other,
}

/// Check that the Nixpkgs attribute values corresponding to the packages in pkgs/by-name are
/// of the form `callPackage <package_file> { ... }`.
/// See the `eval.nix` file for how this is achieved on the Nix side
pub fn check_values(
    nixpkgs_path: &Path,
    package_names: Vec<String>,
    eval_accessible_paths: &[&Path],
) -> 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.
    let attrs_file = NamedTempFile::new().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).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")
        .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())
        // Clear NIX_PATH to be sure it doesn't influence the result
        .env_remove("NIX_PATH")
        .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);

    // Also add extra paths that need to be accessible
    for path in eval_accessible_paths {
        command.arg("-I");
        command.arg(path);
    }
    command.args(["-I", &expr_path]);
    command.arg(expr_path);

    let result = command
        .output()
        .context(format!("Failed to run command {command:?}"))?;

    if !result.status.success() {
        anyhow::bail!("Failed to run command {command:?}");
    }
    // Parse the resulting JSON value
    let actual_files: HashMap<String, AttributeInfo> = serde_json::from_slice(&result.stdout)
        .context(format!(
            "Failed to deserialise {}",
            String::from_utf8_lossy(&result.stdout)
        ))?;

    Ok(
        validation::sequence(package_names.into_iter().map(|package_name| {
            let relative_package_file = structure::relative_file_for_package(&package_name);
            let absolute_package_file = nixpkgs_path.join(&relative_package_file);

            if let Some(attribute_info) = actual_files.get(&package_name) {
                let check_result = if !attribute_info.is_derivation {
                    NixpkgsProblem::NonDerivation {
                        relative_package_file: relative_package_file.clone(),
                        package_name: package_name.clone(),
                    }
                    .into()
                } else {
                    Success(())
                };

                let check_result = check_result.and(match &attribute_info.variant {
                    AttributeVariant::AutoCalled => Success(ratchet::Package {
                        empty_non_auto_called: ratchet::EmptyNonAutoCalled::Valid,
                    }),
                    AttributeVariant::CallPackage { path, empty_arg } => {
                        let correct_file = if let Some(call_package_path) = path {
                            absolute_package_file == *call_package_path
                        } else {
                            false
                        };

                        if correct_file {
                            Success(ratchet::Package {
                                // Empty arguments for non-auto-called packages are not allowed anymore.
                                empty_non_auto_called: if *empty_arg {
                                    ratchet::EmptyNonAutoCalled::Invalid
                                } else {
                                    ratchet::EmptyNonAutoCalled::Valid
                                },
                            })
                        } else {
                            NixpkgsProblem::WrongCallPackage {
                                relative_package_file: relative_package_file.clone(),
                                package_name: package_name.clone(),
                            }
                            .into()
                        }
                    }
                    AttributeVariant::Other => NixpkgsProblem::WrongCallPackage {
                        relative_package_file: relative_package_file.clone(),
                        package_name: package_name.clone(),
                    }
                    .into(),
                });

                check_result.map(|value| (package_name.clone(), value))
            } else {
                NixpkgsProblem::UndefinedAttr {
                    relative_package_file: relative_package_file.clone(),
                    package_name: package_name.clone(),
                }
                .into()
            }
        }))
        .map(|elems| ratchet::Nixpkgs {
            packages: elems.into_iter().collect(),
        }),
    )
}