1
1
mirror of https://github.com/oxalica/nil.git synced 2024-11-22 11:22:46 +03:00

Inject NixOS options

This commit is contained in:
oxalica 2023-03-13 15:34:49 +08:00
parent 9a043a5039
commit 5b99f7aab5
12 changed files with 339 additions and 12 deletions

1
Cargo.lock generated
View File

@ -332,6 +332,7 @@ dependencies = [
"serde",
"serde_json",
"serde_repr",
"syntax",
]
[[package]]

View File

@ -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<FlakeGraph>;
#[salsa::input]
fn nixos_options(&self) -> Arc<NixosOptions>;
}
fn source_root_flake_info(db: &dyn SourceDatabase, sid: SourceRootId) -> Option<Arc<FlakeInfo>> {
@ -250,6 +254,7 @@ pub struct Change {
pub flake_graph: Option<FlakeGraph>,
pub roots: Option<Vec<SourceRoot>>,
pub file_changes: Vec<(FileId, Arc<str>)>,
pub nixos_options: Option<NixosOptions>,
}
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<SourceRoot>) {
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) {

View File

@ -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<char>, 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");

View File

@ -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
}
}

View File

@ -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))
}

View File

@ -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<InferenceResult>;
#[salsa::input]
#[salsa::invoke(options::options_to_config_ty)]
fn nixos_config_ty(&self) -> Ty;
}

View File

@ -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<dyn FnOnce(&mut Server, Response) + 'static>;
type Task = Box<dyn FnOnce() -> Event + Send + 'static>;
@ -33,6 +36,7 @@ enum Event {
},
ClientExited,
LoadFlake(Result<LoadFlakeResult>),
NixosOptions(Result<NixosOptions>),
}
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(())
}

View File

@ -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 = <Arc<str>>::from(text);

View File

@ -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" }

View File

@ -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";

View File

@ -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

View File

@ -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<NixosOptions> {
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<Doc>,
#[serde(default)]
pub declarations: Vec<String>,
#[serde(default)]
pub read_only: bool,
#[serde(rename = "type")]
pub ty: Option<String>,
pub default: Option<Value>,
pub example: Option<Value>,
#[serde(default)]
pub related_packages: Vec<RelatedPackage>,
#[serde(default)]
pub children: HashMap<String, NixosOptions>,
}
#[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<String>,
pub comment: Option<String>,
}
impl<'de> de::Deserialize<'de> for RelatedPackage {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Repr {
Name(String),
Path(Vec<String>),
Full {
name: Option<String>,
path: Option<Vec<String>>,
comment: Option<String>,
},
}
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",
"<nixpkgs>",
])
.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"),
);
}
}