mirror of
https://github.com/oxalica/nil.git
synced 2024-11-25 18:41:40 +03:00
Inject NixOS options
This commit is contained in:
parent
9a043a5039
commit
5b99f7aab5
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -332,6 +332,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -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) {
|
||||
|
@ -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");
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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" }
|
||||
|
@ -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";
|
||||
|
93
crates/nix-interop/src/nixos_options.nix
Normal file
93
crates/nix-interop/src/nixos_options.nix
Normal 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
|
162
crates/nix-interop/src/nixos_options.rs
Normal file
162
crates/nix-interop/src/nixos_options.rs
Normal 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"),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user