From ee2ff825c51d47bbf0160cf8bb1bedc78b997efc Mon Sep 17 00:00:00 2001 From: oxalica Date: Mon, 10 Jul 2023 11:24:47 +0800 Subject: [PATCH] Import docs of builtin constants for Nix >= 2.17 Follow up of #94. --- crates/builtin/build.rs | 174 ++++++++++++++++++++++++++++++-------- crates/builtin/src/lib.rs | 14 ++- 2 files changed, 147 insertions(+), 41 deletions(-) diff --git a/crates/builtin/build.rs b/crates/builtin/build.rs index 09d7bac..24fc849 100644 --- a/crates/builtin/build.rs +++ b/crates/builtin/build.rs @@ -8,7 +8,7 @@ fn main() { // Disable rebuild when source files changed. println!("cargo:rerun-if-changed=build.rs"); - let builtin_names: Vec = Command::new("nix") + let builtins_attr_names: Vec = Command::new("nix") .args([ "eval", "--experimental-features", @@ -26,7 +26,7 @@ fn main() { // Here we run them in parallel. There are hundreds of names to test. #[allow(clippy::needless_collect)] let global_names: Vec = { - builtin_names + builtins_attr_names .iter() .map(|name| { Command::new("nix") @@ -52,50 +52,127 @@ fn main() { .collect() }; - // Use a secret subcommand `__dump-language` to dump Nix builtins with documentations. - // It is introduced since Nix 2.17 in - // https://github.com/NixOS/nix/commit/22b278e011ab9c1328749a126514c57b89a39173#diff-20a8b5b2a231db80eab27840bd32ac0214aa0c4e9e923e649d3d741c3da77b48L355 - let builtins_dump: DumpBuiltins = Command::new("nix") - .arg("__dump-language") - .json::() - .map_or_else( - |_| { - // Fallback to the older command `__dump-builtins` so that the package - // doesn't fail to build for people using older versions of nix - // (introduced in 2.4) - // https://github.com/NixOS/nix/commit/0f314f3c2594e80322c675b70a61dcfda11bf423#diff-20a8b5b2a231db80eab27840bd32ac0214aa0c4e9e923e649d3d741c3da77b48R187 - Command::new("nix") - .arg("__dump-builtins") - .json::() - }, - |v| Ok(v.builtins), - ) - .expect("Failed to dump builtins"); + let mut builtin_infos = dump_builtin_infos(); + builtin_infos.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); + assert_eq!( + builtin_infos.windows(2).find(|w| w[0].name == w[1].name), + None, + "no duplicated builtins", + ); let mut phf_gen = phf_codegen::Map::<&'static str>::new(); - for (name, is_global) in builtin_names.iter().zip(&global_names) { + for (name, is_global) in builtins_attr_names.iter().zip(&global_names) { let name = &**name; - let kind = match name { - "builtins" => "Attrset", - "true" | "false" | "null" => "Const", - _ => "Function", + let BuiltinInfo { + name: _, + kind, + doc, + args, + impure_only, + experimental_feature, + } = match builtin_infos.binary_search_by(|probe| (*probe.name).cmp(name)) { + Ok(i) => builtin_infos[i].clone(), + Err(_) => BuiltinInfo { + name: String::new(), // Not used. + kind: guess_name_kind(name).into(), + doc: "(No documentation from Nix)".into(), + args: Vec::new(), + impure_only: false, + experimental_feature: None, + }, }; - let args = builtins_dump - .get(name) - .map(|b @ DumpBuiltin { args, arity, .. }| { - assert_eq!(args.len(), *arity, "Arity mismatch: {b:?}"); - args.iter().flat_map(|arg| [" ", arg]).collect::() - }) - .unwrap_or_default(); - let summary = format!("`builtins.{name}{args}`"); - let doc = builtins_dump.get(name).map(|b| &b.doc); - phf_gen.entry(name, &format!("crate::Builtin {{ kind: crate::BuiltinKind::{kind}, is_global: {is_global}, summary: {summary:?}, doc: {doc:?} }}")); + + let summary = ["`builtins.", name] + .into_iter() + .chain(args.iter().flat_map(|arg| [" ", arg])) + .chain(Some("`")) + .collect::(); + let rhs = format!( + "crate::Builtin {{ + kind: crate::BuiltinKind::{kind}, + is_global: {is_global}, + summary: {summary:?}, + doc: Some({doc:?}), + impure_only: {impure_only}, + experimental_feature: {experimental_feature:?}, + }}" + ); + phf_gen.entry(name, &rhs); } let path = Path::new(&env::var("OUT_DIR").unwrap()).join("generated.expr"); fs::write(path, phf_gen.build().to_string()).unwrap(); } +fn dump_builtin_infos() -> Vec { + // Use a secret subcommand `__dump-language` to dump Nix builtins with documentations. + // It is introduced since Nix 2.17 in + // https://github.com/NixOS/nix/commit/22b278e011ab9c1328749a126514c57b89a39173#diff-20a8b5b2a231db80eab27840bd32ac0214aa0c4e9e923e649d3d741c3da77b48L355 + if let Ok(lang) = Command::new("nix") + .arg("__dump-language") + .json::() + { + return std::iter::empty() + .chain( + lang.builtins + .into_iter() + .map(|(name, builtin)| BuiltinInfo { + name, + kind: "Function".into(), + doc: builtin.doc, + args: builtin.args, + impure_only: false, + experimental_feature: builtin.experimental_feature, + }), + ) + .chain(lang.constants.into_iter().map(|(name, constant)| { + let kind = if constant.type_.eq_ignore_ascii_case("set") { + "Attrset" + } else { + "Const" + }; + BuiltinInfo { + name, + kind: kind.into(), + doc: constant.doc, + args: Vec::new(), + impure_only: constant.impure_only, + experimental_feature: None, + } + })) + .collect(); + } + + // Fallback to the older command `__dump-builtins` so that the package + // doesn't fail to build for people using older versions of nix + // (introduced in 2.4) + // https://github.com/NixOS/nix/commit/0f314f3c2594e80322c675b70a61dcfda11bf423#diff-20a8b5b2a231db80eab27840bd32ac0214aa0c4e9e923e649d3d741c3da77b48R187 + let builtins = Command::new("nix") + .arg("__dump-builtins") + .json::() + .expect("failed to dump builtin docs"); + + builtins + .into_iter() + .map(|(name, builtin)| BuiltinInfo { + kind: guess_name_kind(&name).into(), + name, + doc: builtin.doc, + args: builtin.args, + impure_only: false, + experimental_feature: None, + }) + .collect() +} + +fn guess_name_kind(name: &str) -> &'static str { + match name { + "builtins" => "Attrset", + "true" | "false" | "null" => "Const", + _ => "Function", + } +} + trait CommandExt { fn json Deserialize<'de>>(&mut self) -> Result>; } @@ -110,17 +187,40 @@ impl CommandExt for Command { } } +#[derive(Debug, Clone, PartialEq, Eq)] +struct BuiltinInfo { + name: String, + kind: String, + doc: String, + args: Vec, + impure_only: bool, + experimental_feature: Option, +} + #[derive(Debug, Deserialize)] struct DumpLanguage { builtins: DumpBuiltins, + constants: BTreeMap, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct DumpConstant { + doc: String, + impure_only: bool, + // TODO + #[serde(rename = "type")] + type_: String, } // Keep names sorted. type DumpBuiltins = BTreeMap; #[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] struct DumpBuiltin { args: Vec, - arity: usize, doc: String, + #[serde(default)] + experimental_feature: Option, } diff --git a/crates/builtin/src/lib.rs b/crates/builtin/src/lib.rs index f9d6765..719fe2b 100644 --- a/crates/builtin/src/lib.rs +++ b/crates/builtin/src/lib.rs @@ -4,6 +4,8 @@ pub struct Builtin { pub is_global: bool, pub summary: &'static str, pub doc: Option<&'static str>, + pub impure_only: bool, + pub experimental_feature: Option<&'static str>, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -23,15 +25,17 @@ mod tests { #[test] fn sanity() { - assert_eq!( + assert!(matches!( ALL_BUILTINS["true"], Builtin { kind: BuiltinKind::Const, is_global: true, summary: "`builtins.true`", - doc: None, - }, - ); + doc: _, + impure_only: false, + experimental_feature: None, + } + )); assert_eq!( ALL_BUILTINS["attrNames"], @@ -46,6 +50,8 @@ alphabetically sorted list. For instance, `builtins.attrNames { y = 1; x = \"foo\"; }` evaluates to `[ \"x\" \"y\" ]`.\ " ), + impure_only: false, + experimental_feature: None, } ); }