From 5b99f7aab5d2ed11ddd83fbd882a26196d96886a Mon Sep 17 00:00:00 2001 From: oxalica Date: Mon, 13 Mar 2023 15:34:49 +0800 Subject: [PATCH] Inject NixOS options --- Cargo.lock | 1 + crates/ide/src/base.rs | 12 ++ crates/ide/src/ide/completion.rs | 23 +++- crates/ide/src/ide/mod.rs | 5 +- crates/ide/src/tests.rs | 7 +- crates/ide/src/ty/mod.rs | 3 +- crates/nil/src/server.rs | 38 ++++++ crates/nil/src/vfs.rs | 5 + crates/nix-interop/Cargo.toml | 1 + crates/nix-interop/src/lib.rs | 1 + crates/nix-interop/src/nixos_options.nix | 93 +++++++++++++ crates/nix-interop/src/nixos_options.rs | 162 +++++++++++++++++++++++ 12 files changed, 339 insertions(+), 12 deletions(-) create mode 100644 crates/nix-interop/src/nixos_options.nix create mode 100644 crates/nix-interop/src/nixos_options.rs diff --git a/Cargo.lock b/Cargo.lock index 45e6899..8a5f999 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -332,6 +332,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", + "syntax", ] [[package]] diff --git a/crates/ide/src/base.rs b/crates/ide/src/base.rs index a5a4da4..d1a6185 100644 --- a/crates/ide/src/base.rs +++ b/crates/ide/src/base.rs @@ -1,3 +1,4 @@ +use nix_interop::nixos_options::NixosOptions; use salsa::Durability; use std::collections::HashMap; use std::fmt; @@ -239,6 +240,9 @@ pub trait SourceDatabase { #[salsa::input] fn flake_graph(&self) -> Arc; + + #[salsa::input] + fn nixos_options(&self) -> Arc; } fn source_root_flake_info(db: &dyn SourceDatabase, sid: SourceRootId) -> Option> { @@ -250,6 +254,7 @@ pub struct Change { pub flake_graph: Option, pub roots: Option>, pub file_changes: Vec<(FileId, Arc)>, + pub nixos_options: Option, } impl Change { @@ -261,6 +266,10 @@ impl Change { self.flake_graph = Some(graph); } + pub fn set_nixos_options(&mut self, opts: NixosOptions) { + self.nixos_options = Some(opts); + } + pub fn set_roots(&mut self, roots: Vec) { self.roots = Some(roots); } @@ -273,6 +282,9 @@ impl Change { if let Some(flake_graph) = self.flake_graph { db.set_flake_graph_with_durability(Arc::new(flake_graph), Durability::MEDIUM); } + if let Some(opts) = self.nixos_options { + db.set_nixos_options_with_durability(Arc::new(opts), Durability::MEDIUM); + } if let Some(roots) = self.roots { u32::try_from(roots.len()).expect("Length overflow"); for (sid, root) in (0u32..).map(SourceRootId).zip(roots) { diff --git a/crates/ide/src/ide/completion.rs b/crates/ide/src/ide/completion.rs index affce6a..8306683 100644 --- a/crates/ide/src/ide/completion.rs +++ b/crates/ide/src/ide/completion.rs @@ -485,10 +485,12 @@ fn can_complete(prefix: &str, replace: &str) -> bool { #[cfg(test)] mod tests { + use std::sync::Arc; + use crate::base::SourceDatabase; use crate::tests::TestDB; - use crate::TyDatabase; use expect_test::{expect, Expect}; + use nix_interop::nixos_options::NixosOptions; #[track_caller] fn check_no(fixture: &str, label: &str) { @@ -501,10 +503,21 @@ mod tests { #[track_caller] fn check_trigger(fixture: &str, trigger_char: Option, label: &str, expect: Expect) { let (mut db, f) = TestDB::from_fixture(fixture).unwrap(); - db.set_nixos_config_ty(ty!({ - "nix": { - "enable": bool - } + db.set_nixos_options(Arc::new(NixosOptions { + children: <_>::from_iter([( + "nix".into(), + NixosOptions { + children: <_>::from_iter([( + "enable".into(), + NixosOptions { + ty: Some("boolean".into()), + ..NixosOptions::default() + }, + )]), + ..NixosOptions::default() + }, + )]), + ..NixosOptions::default() })); let compes = super::completions(&db, f[0], trigger_char).expect("No completion"); diff --git a/crates/ide/src/ide/mod.rs b/crates/ide/src/ide/mod.rs index 3fbec24..1445ee3 100644 --- a/crates/ide/src/ide/mod.rs +++ b/crates/ide/src/ide/mod.rs @@ -16,8 +16,7 @@ use crate::base::SourceDatabaseStorage; use crate::def::DefDatabaseStorage; use crate::ty::TyDatabaseStorage; use crate::{ - Change, Diagnostic, FileId, FilePos, FileRange, FileSet, SourceRoot, TyDatabase, VfsPath, - WorkspaceEdit, + Change, Diagnostic, FileId, FilePos, FileRange, FileSet, SourceRoot, VfsPath, WorkspaceEdit, }; use nix_interop::DEFAULT_IMPORT_FILE; use salsa::{Database, Durability, ParallelDatabase}; @@ -83,7 +82,7 @@ impl Default for RootDatabase { .set_lru_capacity(DEFAULT_LRU_CAP); db.set_flake_graph_with_durability(Arc::default(), Durability::MEDIUM); - db.set_nixos_config_ty_with_durability(ty!({}), Durability::MEDIUM); + db.set_nixos_options_with_durability(Arc::default(), Durability::MEDIUM); db } } diff --git a/crates/ide/src/tests.rs b/crates/ide/src/tests.rs index a1260b0..8b17ff3 100644 --- a/crates/ide/src/tests.rs +++ b/crates/ide/src/tests.rs @@ -2,13 +2,14 @@ use crate::base::SourceDatabaseStorage; use crate::def::DefDatabaseStorage; use crate::ty::TyDatabaseStorage; use crate::{ - Change, DefDatabase, FileId, FilePos, FileRange, FileSet, FlakeGraph, FlakeInfo, SourceRoot, - SourceRootId, TyDatabase, VfsPath, + Change, DefDatabase, FileId, FilePos, FileRange, FileSet, FlakeGraph, FlakeInfo, + SourceDatabase, SourceRoot, SourceRootId, VfsPath, }; use anyhow::{bail, ensure, Context, Result}; use indexmap::IndexMap; use nix_interop::{DEFAULT_IMPORT_FILE, FLAKE_FILE}; use std::collections::HashMap; +use std::sync::Arc; use std::{mem, ops}; use syntax::ast::AstNode; use syntax::{NixLanguage, SyntaxNode, TextRange, TextSize}; @@ -47,7 +48,7 @@ impl TestDB { nodes: HashMap::from_iter(f.flake_info.clone().map(|info| (SourceRootId(0), info))), }; change.set_flake_graph(flake_graph); - db.set_nixos_config_ty(ty!({})); + db.set_nixos_options(Arc::default()); change.apply(&mut db); Ok((db, f)) } diff --git a/crates/ide/src/ty/mod.rs b/crates/ide/src/ty/mod.rs index fd740ab..97a5028 100644 --- a/crates/ide/src/ty/mod.rs +++ b/crates/ide/src/ty/mod.rs @@ -71,6 +71,7 @@ macro_rules! ty { mod display; mod infer; pub mod known; +mod options; mod union_find; #[cfg(test)] @@ -93,7 +94,7 @@ pub trait TyDatabase: DefDatabase { #[salsa::invoke(infer::infer_query)] fn infer(&self, file: FileId) -> Arc; - #[salsa::input] + #[salsa::invoke(options::options_to_config_ty)] fn nixos_config_ty(&self) -> Ty; } diff --git a/crates/nil/src/server.rs b/crates/nil/src/server.rs index a16d420..ae17ef6 100644 --- a/crates/nil/src/server.rs +++ b/crates/nil/src/server.rs @@ -10,6 +10,7 @@ use lsp_types::{ InitializeParams, MessageType, NumberOrString, PublishDiagnosticsParams, ShowMessageParams, Url, }; +use nix_interop::nixos_options::NixosOptions; use nix_interop::{flake_lock, FLAKE_FILE, FLAKE_LOCK_FILE}; use std::backtrace::Backtrace; use std::cell::Cell; @@ -20,6 +21,8 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Once, RwLock}; use std::{fs, panic, thread}; +const NIXOS_OPTIONS_FLAKE_INPUT: &str = "nixpkgs"; + type ReqHandler = Box; type Task = Box Event + Send + 'static>; @@ -33,6 +36,7 @@ enum Event { }, ClientExited, LoadFlake(Result), + NixosOptions(Result), } enum LoadFlakeResult { @@ -230,6 +234,26 @@ impl Server { if missing_inputs { self.show_message(MessageType::WARNING, "Some flake inputs are not available, please run `nix flake archive` to fetch all inputs"); } + + // TODO: A better way to retrieve the nixpkgs for options? + if let Some(nixpkgs_path) = flake_info + .input_store_paths + .get(NIXOS_OPTIONS_FLAKE_INPUT) + .and_then(VfsPath::as_path) + { + let nixpkgs_path = nixpkgs_path.to_owned(); + let nix_binary = self.config.nix_binary.clone(); + tracing::info!("Evaluating NixOS options from {}", nixpkgs_path.display()); + self.task_tx + .send(Box::new(move || { + Event::NixosOptions(nix_interop::nixos_options::eval_all_options( + &nix_binary, + &nixpkgs_path, + )) + })) + .unwrap(); + } + self.vfs.write().unwrap().set_flake_info(Some(flake_info)); self.apply_vfs_change(); } @@ -239,6 +263,20 @@ impl Server { self.apply_vfs_change(); } }, + Event::NixosOptions(ret) => match ret { + // Sanity check. + Ok(opts) if !opts.children.is_empty() => { + tracing::info!("Loaded NixOS options ({} top-level)", opts.children.len()); + self.vfs.write().unwrap().set_nixos_options(opts); + self.apply_vfs_change(); + } + Ok(_) => { + tracing::error!("Empty NixOS options?"); + } + Err(err) => { + tracing::error!("Failed to evalute NixOS options: {err}"); + } + }, } Ok(()) } diff --git a/crates/nil/src/vfs.rs b/crates/nil/src/vfs.rs index a6f25fc..42da891 100644 --- a/crates/nil/src/vfs.rs +++ b/crates/nil/src/vfs.rs @@ -2,6 +2,7 @@ use crate::UrlExt; use anyhow::{ensure, Context, Result}; use ide::{Change, FileId, FileSet, FlakeGraph, FlakeInfo, SourceRoot, SourceRootId, VfsPath}; use lsp_types::Url; +use nix_interop::nixos_options::NixosOptions; use std::collections::HashMap; use std::sync::Arc; use std::{fmt, mem}; @@ -44,6 +45,10 @@ impl Vfs { }); } + pub fn set_nixos_options(&mut self, opts: NixosOptions) { + self.change.set_nixos_options(opts); + } + pub fn set_path_content(&mut self, path: VfsPath, text: String) -> FileId { let (text, line_map) = LineMap::normalize(text); let text = >::from(text); diff --git a/crates/nix-interop/Cargo.toml b/crates/nix-interop/Cargo.toml index 4e8910f..1d0ca49 100644 --- a/crates/nix-interop/Cargo.toml +++ b/crates/nix-interop/Cargo.toml @@ -10,3 +10,4 @@ anyhow = "1.0.68" serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.91" serde_repr = "0.1.10" +syntax = { path = "../syntax" } diff --git a/crates/nix-interop/src/lib.rs b/crates/nix-interop/src/lib.rs index 77433b2..1c73eef 100644 --- a/crates/nix-interop/src/lib.rs +++ b/crates/nix-interop/src/lib.rs @@ -1,6 +1,7 @@ //! Nix defined file structures and interoperation with Nix. pub mod eval; pub mod flake_lock; +pub mod nixos_options; pub const DEFAULT_IMPORT_FILE: &str = "default.nix"; pub const FLAKE_FILE: &str = "flake.nix"; diff --git a/crates/nix-interop/src/nixos_options.nix b/crates/nix-interop/src/nixos_options.nix new file mode 100644 index 0000000..88f8a9e --- /dev/null +++ b/crates/nix-interop/src/nixos_options.nix @@ -0,0 +1,93 @@ +# References: +# - nixos/lib/eval-cacheable-options.nix +# - nixos/lib/make-options-doc/default.nix +nixpkgs: +let + lib = import (nixpkgs + "/lib"); + modulePath = nixpkgs + "/nixos/modules"; + + inherit (builtins) filter mapAttrs isPath isFunction functionArgs addErrorContext; + inherit (lib) evalModules trivial optionalAttrs optionals filterAttrs; + inherit (lib.options) unknownModule renderOptionValue showOption; + + # Dummy `pkgs`. + pkgs = import (nixpkgs + "/pkgs/pkgs-lib") { + inherit lib; + pkgs = null; + }; + utils = import (nixpkgs + "/nixos/lib/utils.nix") { + inherit config lib; + pkgs = null; + }; + + modules = + (filter canCacheDocs + (import (modulePath + "/module-list.nix"))); + + # From `nixos/modules/misc/documentation.nix`. + canCacheDocs = m: + let + f = import m; + instance = f (mapAttrs (n: _: abort "evaluating ${n} for `meta` failed") (functionArgs f)); + in + isPath m + && isFunction f + && instance ? options + && instance.meta.buildDocsInSandbox or true; + + config = { + _module.check = false; + _module.args = {}; + system.stateVersion = trivial.release; + }; + eval = evalModules { + modules = modules ++ [ config ]; + specialArgs = { + inherit config pkgs utils; + }; + }; + + # Modified from `lib.optionAttrSetToDocList`. + normalizeOptions = opt: let + # visible: true | false | "shallow" + visible = (opt.visible or true != false) && !(opt.internal or false); + + opt' = { + description = opt.description or null; + declarations = filter (x: x != unknownModule) opt.declarations; + readOnly = opt.readOnly or false; + type = opt.type.description or "unspecified"; + example = + if opt ? example then + renderOptionValue opt.example + else + null; + default = + if opt ? default then + renderOptionValue (opt.defaultText or opt.default) + else + null; + relatedPackages = + optionals (opt.relatedPackages or null != null) + opt.relatedPackages; + + # TODO: Submodules. + }; + in + if visible then + opt' + else + null; + + normalizeOptionOrSet = opts: + if opts._type or null == "option" then + normalizeOptions opts + else { + children = + filterAttrs (k: v: !isNull v) + (mapAttrs (_: normalizeOptionOrSet) opts); + }; + +in + normalizeOptionOrSet + eval.options diff --git a/crates/nix-interop/src/nixos_options.rs b/crates/nix-interop/src/nixos_options.rs new file mode 100644 index 0000000..2c34d3f --- /dev/null +++ b/crates/nix-interop/src/nixos_options.rs @@ -0,0 +1,162 @@ +use std::collections::HashMap; +use std::path::Path; +use std::process::{Command, Stdio}; + +use anyhow::{ensure, Context, Result}; +use serde::{de, Deserialize}; +use syntax::semantic::escape_string; + +pub fn eval_all_options(nix_command: &Path, nixpkgs_path: &Path) -> Result { + let nixpkgs_path = nixpkgs_path + .to_str() + .with_context(|| format!("Invalid path to nixpkgs: {}", nixpkgs_path.display()))?; + + let output = Command::new(nix_command) + .args([ + "eval", + "--experimental-features", + "nix-command", + "--read-only", + "--impure", + "--json", + "--show-trace", + "--expr", + &escape_string(nixpkgs_path), + // Workaround: `--argstr` is broken currently. + // https://github.com/NixOS/nix/issues/2678 + "--apply", + include_str!("./nixos_options.nix"), + ]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .context("Failed to spawn `nix`")?; + + ensure!( + output.status.success(), + "Nix eval failed with {}. Stderr:\n{}", + output.status, + String::from_utf8_lossy(&output.stderr), + ); + + let val = serde_json::from_slice(&output.stdout)?; + Ok(val) +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NixosOptions { + pub description: Option, + #[serde(default)] + pub declarations: Vec, + #[serde(default)] + pub read_only: bool, + #[serde(rename = "type")] + pub ty: Option, + pub default: Option, + pub example: Option, + #[serde(default)] + pub related_packages: Vec, + + #[serde(default)] + pub children: HashMap, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(tag = "_type")] +pub enum Doc { + #[serde(rename = "mdDoc")] + Markdown { text: String }, + #[serde(other)] + Other, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(tag = "_type")] +pub enum Value { + #[serde(rename = "literalExpression")] + Expression { text: String }, + #[serde(rename = "literalMD")] + Markdown { text: String }, + #[serde(other)] + Other, +} + +// https://github.com/NixOS/nixpkgs/blob/28c1aac72e3aef70b8c898ea9c16d5907f9eae22/nixos/lib/make-options-doc/default.nix#L61 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RelatedPackage { + pub path: Vec, + pub comment: Option, +} + +impl<'de> de::Deserialize<'de> for RelatedPackage { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum Repr { + Name(String), + Path(Vec), + Full { + name: Option, + path: Option>, + comment: Option, + }, + } + + Ok(match Repr::deserialize(deserializer)? { + Repr::Name(name) => Self { + path: vec![name], + comment: None, + }, + Repr::Path(path) => Self { + path, + comment: None, + }, + Repr::Full { + name, + path, + comment, + } => Self { + path: path.or_else(|| Some(vec![name?])).unwrap_or_default(), + comment, + }, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[ignore = "requires using 'nix' and 'nixpkgs'"] + fn nixos_options() { + let output = Command::new("nix") + .args([ + "eval", + "--experimental-features", + "nix-command", + "--impure", + "--expr", + "", + ]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .output() + .unwrap(); + assert!(output.status.success()); + let nixpkgs_path = String::from_utf8(output.stdout).unwrap(); + let opts = eval_all_options("nix".as_ref(), nixpkgs_path.trim().as_ref()).unwrap(); + + // Sanity check. + assert_eq!( + opts.children["nix"].children["enable"].ty.as_deref(), + Some("boolean"), + ); + } +}