Skip to content

Commit 04213ab

Browse files
fix(nix_flake_fmt): handle flakes with a formatter package (#272)
* refactor(fix nix_flake_fmt): split `find_nix_fmt` into smaller functions This should make things a bit more readable without changing the behavior. * refactor(nix_flake_fmt): pass data as JSON rather than ad-hoc lines This makes the communication between our lua code and the `nix eval` subprocess a bit clearer. * fix(nix_flake_fmt): handle flakes with a `formatter` package nixpkgs [recently add a `nix fmt` entrypoint](NixOS/nixpkgs@374e6bc), and I was excited to be able to use `nix_flake_fmt` in the project. I was disappointed to find that it doesn't actually work in nixpkgs, though :( The problem boils down to how `nix eval .#formatter` behaves if your flake defines a `formatter` package. See: ```console $ nix eval nixpkgs#formatter «derivation /nix/store/nadgq8kci2an9d3ddz1lk662gl53d8yz-formatter-0.4.0.drv» ``` This is *not* the `nix fmt` entrypoint, it's a [completely unrelated package](NixOS/nixpkgs@b99357e). Here's the `nix fmt` entrypoint: ```console $ nix repl nixpkgs nix-repl> formatter.x86_64-linux «derivation /nix/store/rza8g2jxr1h7nd58qq3ryfz2zdqnggga-treefmt.drv» ``` This simple `nix repl` session is quite challenging to port to `nix eval`, though. After quite a bit of futzing around, I found that I could accomplish this with a combination of `nix flake metadata` and `builtins.getFlake`: ``` $ nix flake metadata nixpkgs --json | jq .resolvedUrl "github:NixOS/nixpkgs/nixpkgs-unstable" $ nix eval --expr "(builtins.getFlake "github:NixOS/nixpkgs/nixpkgs-unstable").formatter.x86_64-linux" --impure «derivation /nix/store/rza8g2jxr1h7nd58qq3ryfz2zdqnggga-treefmt.drv» ``` * fix: ensure we always return a result, and clear the progress notification There were 2 bugs here: - In the `drv_path == nil` path, we'd abort execution without returning a result by calling `done(...)`. - In that codepath and another, we'd return a `nil` result without clearing the progress notification. There might be bugs here if we are running multiple formats in parallel. I think that's OK to address in the future if it proves to be a problem. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 751349f commit 04213ab

File tree

1 file changed

+125
-62
lines changed

1 file changed

+125
-62
lines changed

lua/null-ls/builtins/formatting/nix_flake_fmt.lua

Lines changed: 125 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ local log = require("null-ls.logger")
44
local client = require("null-ls.client")
55

66
local FORMATTING = methods.internal.FORMATTING
7+
local NOTIFICATION_TITLE = "discovering `nix fmt` entrypoint"
8+
local NOTIFICATION_TOKEN = "nix-flake-fmt-discovery"
79

810
--- Asynchronously computes the command that `nix fmt` would run, or nil if
911
--- we're not in a flake with a formatter, or if we fail to discover the
@@ -39,25 +41,7 @@ local find_nix_fmt = function(opts, done)
3941
end, 0)
4042
end, 1)
4143

42-
async.run(function()
43-
local title = "discovering `nix fmt` entrypoint"
44-
local progress_token = "nix-flake-fmt-discovery"
45-
46-
client.send_progress_notification(progress_token, {
47-
kind = "begin",
48-
title = title,
49-
})
50-
51-
local root = opts.root
52-
53-
-- Discovering `currentSystem` here lets us keep the *next* eval pure.
54-
-- We want to keep that part pure as a performance improvement: an impure
55-
-- eval that references the flake would copy *all* files (including
56-
-- gitignored files!), which can be quite expensive if you've got many GiB
57-
-- of artifacts in the directory. This optimization can probably go away
58-
-- once the [Lazy trees PR] lands.
59-
--
60-
-- [Lazy trees PR]: https://github.com/NixOS/nix/pull/6530
44+
local get_current_system = function()
6145
local status, stdout_lines, stderr_lines = run_job({
6246
command = "nix",
6347
args = {
@@ -74,15 +58,59 @@ local find_nix_fmt = function(opts, done)
7458
vim.defer_fn(function()
7559
log:warn(string.format("unable to discover builtins.currentSystem from nix. stderr: %s", stderr))
7660
end, 0)
77-
done(nil)
7861
return
7962
end
8063

8164
local nix_current_system = stdout_lines[1]
65+
return nix_current_system
66+
end
67+
68+
local get_flake_ref = function(root)
69+
local status, stdout_lines, stderr_lines = run_job({
70+
command = "nix",
71+
args = {
72+
"--extra-experimental-features",
73+
"nix-command flakes",
74+
"flake",
75+
"metadata",
76+
"--json",
77+
root,
78+
},
79+
})
80+
81+
if status ~= 0 then
82+
local stderr = table.concat(stderr_lines, "\n")
83+
vim.defer_fn(function()
84+
log:warn(string.format("unable to get flake ref for '%s'. stderr: %s", root, stderr))
85+
end, 0)
86+
return
87+
end
88+
89+
local stdout = table.concat(stdout_lines, "\n")
90+
local metadata = vim.json.decode(stdout)
91+
local flake_ref = metadata.resolvedUrl
92+
if flake_ref == nil then
93+
vim.defer_fn(function()
94+
log:warn(
95+
string.format("flake metadata does not have a 'resolvedUrl'. metadata: %s", vim.inspect(metadata))
96+
)
97+
end, 0)
98+
return
99+
end
100+
101+
return flake_ref
102+
end
82103

104+
local evaluate_flake_formatter = function(root)
105+
local nix_current_system = get_current_system()
106+
if nix_current_system == nil then
107+
return
108+
end
109+
local flake_ref = get_flake_ref(root)
83110
local eval_nix_formatter = [[
84111
let
85-
currentSystem = "]] .. nix_current_system .. [[";
112+
system = "]] .. nix_current_system .. [[";
113+
flake = builtins.getFlake "]] .. flake_ref .. [[";
86114
# Various functions vendored from nixpkgs lib (to avoid adding a
87115
# dependency on nixpkgs).
88116
lib = rec {
@@ -96,76 +124,85 @@ local find_nix_fmt = function(opts, done)
96124
# getExe is simplified to assume meta.mainProgram is specified.
97125
getExe = x: getExe' x x.meta.mainProgram;
98126
};
127+
result =
128+
if flake ? formatter then
129+
if flake.formatter ? ${system} then
130+
let
131+
formatter = flake.formatter.${system};
132+
drv = formatter.drvPath;
133+
bin = lib.getExe formatter;
134+
in
135+
{ inherit drv bin; }
136+
else
137+
{ error = "this flake does not define a formatter for system: ${system}"; }
138+
else
139+
{ error = "this flake does not define any formatters"; };
99140
in
100-
formatterBySystem:
101-
if formatterBySystem ? ${currentSystem} then
102-
let
103-
formatter = formatterBySystem.${currentSystem};
104-
drv = formatter.drvPath;
105-
bin = lib.getExe formatter;
106-
in
107-
drv + "\n" + bin + "\n"
108-
else
109-
""
141+
builtins.toJSON result
110142
]]
111143

112-
client.send_progress_notification(progress_token, {
144+
client.send_progress_notification(NOTIFICATION_TOKEN, {
113145
kind = "report",
114-
title = title,
146+
title = NOTIFICATION_TITLE,
115147
message = "evaluating",
116148
})
117-
status, stdout_lines, stderr_lines = run_job({
149+
150+
local status, stdout_lines, stderr_lines = run_job({
118151
command = "nix",
119152
args = {
120153
"--extra-experimental-features",
121154
"nix-command flakes",
122155
"eval",
123-
".#formatter",
124156
"--raw",
125-
"--apply",
157+
-- We need `--impure` to be able to call `builtins.getFlake`
158+
-- on an unlocked flake ref.
159+
"--impure",
160+
"--expr",
126161
eval_nix_formatter,
127162
},
128-
cwd = root,
129163
})
130164

131165
if status ~= 0 then
132166
local stderr = table.concat(stderr_lines, "\n")
133167
vim.defer_fn(function()
134168
log:warn(string.format("unable to discover 'nix fmt' command. stderr: %s", stderr))
135169
end, 0)
136-
done(nil)
137170
return
138171
end
139172

140-
if #stdout_lines == 0 then
173+
local stdout = table.concat(stdout_lines, "\n")
174+
local result = vim.json.decode(stdout)
175+
176+
if result.error ~= nil then
141177
vim.defer_fn(function()
142-
log:warn(
143-
string.format("this flake does not define a formatter for your system: %s", nix_current_system)
144-
)
178+
log:warn(result.error)
145179
end, 0)
146-
done(nil)
147180
return
148181
end
149182

150-
-- stdout has 2 lines of output:
151-
-- 1. drv path
152-
-- 2. exe path
153-
local drv_path, nix_fmt_path = unpack(stdout_lines)
183+
local drv_path = result.drv
184+
local nix_fmt_path = result.bin
185+
return drv_path, nix_fmt_path
186+
end
154187

155-
-- Build the derivation. This ensures that `nix_fmt_path` exists.
156-
client.send_progress_notification(progress_token, {
157-
kind = "report",
158-
title = title,
159-
message = "building",
160-
})
161-
status, stdout_lines, stderr_lines = run_job({
188+
local build_derivation = function(options)
189+
if type(options.drv) ~= "string" then
190+
error("missing drv")
191+
elseif type(options.out_link) ~= "string" then
192+
error("missing out_link")
193+
end
194+
195+
local drv_path = options.drv
196+
local out_link = options.out_link
197+
198+
local status, _, stderr_lines = run_job({
162199
command = "nix",
163200
args = {
164201
"--extra-experimental-features",
165202
"nix-command",
166203
"build",
167204
"--out-link",
168-
tmpname(),
205+
out_link,
169206
drv_path .. "^out",
170207
},
171208
})
@@ -175,17 +212,43 @@ local find_nix_fmt = function(opts, done)
175212
vim.defer_fn(function()
176213
log:warn(string.format("unable to build 'nix fmt' entrypoint. stderr: %s", stderr))
177214
end, 0)
178-
done(nil)
179-
return
215+
return false
216+
end
217+
218+
return true
219+
end
220+
221+
async.run(function()
222+
client.send_progress_notification(NOTIFICATION_TOKEN, {
223+
kind = "begin",
224+
title = NOTIFICATION_TITLE,
225+
})
226+
227+
local _done = function(result)
228+
done(result)
229+
client.send_progress_notification(NOTIFICATION_TOKEN, {
230+
kind = "end",
231+
title = NOTIFICATION_TITLE,
232+
message = "done",
233+
})
234+
end
235+
236+
local drv_path, nix_fmt_path = evaluate_flake_formatter(opts.root)
237+
if drv_path == nil then
238+
return _done(nil)
180239
end
181240

182-
client.send_progress_notification(progress_token, {
183-
kind = "end",
184-
title = title,
185-
message = "done",
241+
-- Build the derivation. This ensures that `nix_fmt_path` exists.
242+
client.send_progress_notification(NOTIFICATION_TOKEN, {
243+
kind = "report",
244+
title = NOTIFICATION_TITLE,
245+
message = "building",
186246
})
247+
if not build_derivation({ drv = drv_path, out_link = tmpname() }) then
248+
return _done(nil)
249+
end
187250

188-
done(nix_fmt_path)
251+
return _done(nix_fmt_path)
189252
end)
190253
end
191254

0 commit comments

Comments
 (0)