1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
open Ast
open Builder_utils
(*
--# Build Pipeline Internally
--#
--# Calls `nix-build` on the generated `pipeline.nix` file. Extracts the store
--# path of the result and saves a build log with an exact mapping of node names
--# to artifact paths in the Nix store.
--#
--# @name build_pipeline_internal
--# @param p :: PipelineResult The pipeline AST structure.
--# @return :: Result[String] The output Nix store path or an error string.
--# @family pipeline
--# @export
*)
let nix_build_args = ref []
let default_nix_build_verbose = ref 0
let nix_verbosity_args verbose =
if verbose <= 0 then ["--quiet"]
else List.init (max 0 (verbose - 1)) (fun _ -> "--verbose")
(*
--# Print Failed Node Logs
--#
--# Prints stderr log sections for each failed node by resolving its
--# derivation path through `nix log`.
--#
--# @param drv_paths :: Hashtbl Captured derivation paths keyed by node name.
--# @param errored :: List[String] Node names that failed during the build.
--# @family pipeline
*)
let print_failed_node_logs drv_paths errored =
List.iter
(fun name ->
match Hashtbl.find_opt drv_paths name with
| Some drv_path ->
Printf.eprintf "\n--- Logs for failed node `%s` ---\n%!" name;
let argv = [| "nix"; "log"; drv_path |] in
(match run_command_argv_capture argv with
| Ok output ->
let output = String.trim output in
if output = "" then
Printf.eprintf "(No log output returned for `%s`).\n%!" name
else
Printf.eprintf "%s\n%!" output
| Error msg ->
Printf.eprintf "Failed to fetch logs for `%s`: %s\n%!" name msg)
| None ->
Printf.eprintf "\n--- Logs for failed node `%s` ---\nNo derivation path was captured for this node.\n%!" name)
errored
let build_pipeline_internal ?verbose (p : Ast.pipeline_result) =
let verbose =
match verbose with
| Some level -> level
| None -> !default_nix_build_verbose
in
if not (command_exists "nix-build") then
Error "build_pipeline requires `nix-build` to be available."
else begin
let node_names = List.map fst p.p_exprs in
let statuses = Hashtbl.create (List.length node_names) in
List.iter (fun n -> Hashtbl.add statuses n "Pending") node_names;
let captured_output = Buffer.create 1024 in
let all_args = !nix_build_args @ (nix_verbosity_args verbose) in
let argv = Array.of_list
(["nix-build"; "--impure"; pipeline_nix_path; "-A"; "pipeline_output"; "--no-out-link"] @ all_args)
in
Printf.printf "\nStarting pipeline build...\n";
let contains_substring line pattern =
try
let len_p = String.length pattern in
let len_l = String.length line in
let rec loop i =
if i + len_p > len_l then false
else if String.sub line i len_p = pattern then true
else loop (i + 1)
in
loop 0
with _ -> false
in
let contains_substring_idx line pattern =
let len_p = String.length pattern in
let len_l = String.length line in
let rec loop i =
if i + len_p > len_l then -1
else if String.sub line i len_p = pattern then i
else loop (i + 1)
in
loop 0
in
let drv_paths = Hashtbl.create (List.length node_names) in
let node_has_warnings = Hashtbl.create (List.length node_names) in
let callback line =
Buffer.add_string captured_output line;
Buffer.add_char captured_output '\n';
let line = String.trim line in
(* Each branch performs a single List.find_opt scan to both detect and
identify the matching node, avoiding a separate exists + find_opt double
scan that the previous version performed per line. *)
if String.starts_with ~prefix:"building '/nix/store/" line then (
(* Building a derivation *)
match List.find_opt (fun name -> contains_substring line ("-" ^ name ^ ".drv")) node_names with
| Some name ->
let drv_path =
try
let start_idx = contains_substring_idx line "/nix/store/" in
if start_idx >= 0 then
let sub = String.sub line start_idx (String.length line - start_idx) in
let end_idx = try String.index sub '\'' with _ ->
try String.index sub ' ' with _ ->
String.length sub in
String.sub sub 0 end_idx
else ""
with _ -> ""
in
if drv_path <> "" then Hashtbl.replace drv_paths name drv_path;
if Hashtbl.find statuses name = "Pending" then (
Hashtbl.replace statuses name "Building";
Printf.printf " + %s building\n%!" name
)
| None -> ()
)
else if String.starts_with ~prefix:"/nix/store/" line
&& not (String.ends_with ~suffix:".drv" line) then (
(* Completed: nix-build prints the output store path without ".drv" *)
match List.find_opt (fun name -> contains_substring line ("-" ^ name)) node_names with
| Some name ->
if Hashtbl.find statuses name <> "Completed" &&
Hashtbl.find statuses name <> "SoftFailed" &&
Hashtbl.find statuses name <> "Errored" then (
(* We'll refine this to SoftFailed later if artifact class is VError *)
Hashtbl.replace statuses name "Completed";
Printf.printf " ✓ %s built\n%!" name
)
| None -> ()
)
else (
(* Error detection: only scan for a matching node when error keywords
are present, to avoid a find_opt scan on every non-build/output line. *)
if contains_substring line "error:" || contains_substring line "failed" then (
match List.find_opt (fun name -> contains_substring line ("-" ^ name ^ ".drv")) node_names with
| Some name ->
if Hashtbl.find statuses name <> "Errored" then (
Hashtbl.replace statuses name "Errored";
let drv_path =
match Hashtbl.find_opt drv_paths name with
| Some p -> p
| None ->
try
let start_idx = contains_substring_idx line "/nix/store/" in
if start_idx >= 0 then
let sub = String.sub line start_idx (String.length line - start_idx) in
(* Stop at closing quote or whitespace; do NOT stop at '.' so that
the full ".drv" suffix is preserved in the extracted path. *)
let end_idx = try String.index sub '\'' with _ ->
try String.index sub ' ' with _ ->
String.length sub in
String.sub sub 0 end_idx
else "<path>"
with _ -> "<path>"
in
if drv_path <> "" && drv_path <> "<path>" then Hashtbl.replace drv_paths name drv_path;
Printf.eprintf "\n ✖ Node %s failed! For full logs, run: read_log(\"%s\")\n\n%!" name name
)
| None -> ()
)
)
in
match run_command_stream_argv argv callback with
| Ok status ->
(* Save drv_paths for later tool use (e.g. read_log) *)
let drv_entries = Hashtbl.fold (fun k v acc ->
(Printf.sprintf "\"%s\": \"%s\"" (Serialization.json_escape k) (Serialization.json_escape v)) :: acc
) drv_paths [] in
let drv_json = "{\n " ^ (String.concat ",\n " drv_entries) ^ "\n}" in
ignore (write_file (Filename.concat pipeline_dir "last_build_drvs.json") drv_json);
let output = String.trim (Buffer.contents captured_output) in
(match status with
| Unix.WEXITED 0 when output <> "" ->
let lines = String.split_on_char '\n' output in
let store_paths = List.filter (fun l ->
String.length l > 11 && String.sub l 0 11 = "/nix/store/"
) lines in
(match store_paths with
| [] -> Error "nix-build succeeded but did not return a store path."
| _ ->
let out_path = List.nth store_paths (List.length store_paths - 1) in
(* Reconcile statuses: if nix-build succeeded, check which nodes were built (cached or otherwise) *)
List.iter (fun name ->
let node_path = Filename.concat out_path name in
if Sys.file_exists node_path then (
let class_path = Filename.concat node_path "class" in
let warnings_path = Filename.concat node_path "warnings" in
if Sys.file_exists warnings_path then Hashtbl.replace node_has_warnings name true;
if Hashtbl.find statuses name <> "Completed" &&
Hashtbl.find statuses name <> "SoftFailed" &&
Hashtbl.find statuses name <> "Errored" then (
(match read_file_first_line class_path with
| Some "VError" | Some "Error" -> Hashtbl.replace statuses name "SoftFailed"
| _ -> Hashtbl.replace statuses name "Completed")
) else if Hashtbl.find statuses name = "Completed" then (
(* Refine "Completed" from Nix output if it was actually a soft-fail *)
if (match read_file_first_line class_path with Some "VError" | Some "Error" -> true | _ -> false) then
Hashtbl.replace statuses name "SoftFailed"
)
)
) node_names;
let timestamp = get_timestamp () in
let hash = try
let parts = String.split_on_char '-' (Filename.basename out_path) in
List.hd parts
with _ -> "no_hash"
in
(* Success Summary *)
let completed = List.filter (fun n -> Hashtbl.find statuses n = "Completed") node_names in
let soft_failed = List.filter (fun n -> Hashtbl.find statuses n = "SoftFailed") node_names in
let with_warnings = List.filter (fun n -> Hashtbl.find_opt node_has_warnings n = Some true) node_names in
let total_built = List.length completed + List.length soft_failed in
if List.length soft_failed > 0 || List.length with_warnings > 0 then (
let msg = if List.length soft_failed > 0 then "\n✖ Pipeline build captured node errors" else "\n✓ Pipeline build completed" in
Printf.eprintf "%s [%d succeeded, %d captured errors, %d had warnings]\n%!"
msg (List.length completed) (List.length soft_failed) (List.length with_warnings);
List.iter (fun n -> Printf.eprintf " ! Captured error in node: %s\n%!" n) soft_failed;
List.iter (fun n -> Printf.eprintf " ? Warnings in node: %s\n%!" n) with_warnings
) else
Printf.printf "\n✓ Pipeline build completed [%d/%d nodes built successfully]\n%!"
total_built (List.length node_names);
let log_name = Printf.sprintf "build_log_%s_%s.json" timestamp hash in
let log_path = Filename.concat pipeline_dir log_name in
let log_entries =
List.map (fun (name, _) ->
let node_path = Filename.concat out_path name in
let artifact_path = Filename.concat node_path "artifact" in
let class_path = Filename.concat node_path "class" in
let class_val = match read_file_first_line class_path with Some c -> c | None -> "Unknown" in
let runtime = match List.assoc_opt name p.p_runtimes with Some r -> r | None -> "T" in
let serializer_expr = match List.assoc_opt name p.p_serializers with Some s -> s | None -> Ast.mk_expr (Ast.Var "default") in
let serializer = Nix_unparse.expr_to_string serializer_expr in
let deps = match List.assoc_opt name p.p_deps with Some d -> d| None -> [] in
let status = Hashtbl.find statuses name in
let success = if status = "SoftFailed" then "false" else "true" in
let has_warns = if Hashtbl.find_opt node_has_warnings name = Some true then "true" else "false" in
Serialization.json_dict [
("node", "\"" ^ Serialization.json_escape name ^ "\"");
("path", "\"" ^ Serialization.json_escape artifact_path ^ "\"");
("runtime", "\"" ^ Serialization.json_escape runtime ^ "\"");
("serializer", "\"" ^ Serialization.json_escape serializer ^ "\"");
("class", "\"" ^ Serialization.json_escape class_val ^ "\"");
("dependencies", Serialization.json_list deps);
("success", success);
("warnings", has_warns)
]
) p.p_exprs
in
let log_json = Serialization.json_dict [
("timestamp", "\"" ^ timestamp ^ "\"");
("hash", "\"" ^ hash ^ "\"");
("out_path", "\"" ^ out_path ^ "\"");
("nodes", "[\n" ^ (String.concat ",\n" log_entries) ^ "\n]")
] in
(match write_file log_path log_json with
| Error msg -> Error ("Failed to write build log: " ^ msg)
| Ok () ->
Ok out_path))
| Unix.WEXITED 0 ->
Error "nix-build succeeded but did not return an output path."
| _ ->
let errored = List.filter (fun n -> Hashtbl.find statuses n = "Errored") node_names in
let error_summary =
if List.length errored > 0 then
Printf.sprintf "%d nodes errored: %s" (List.length errored) (String.concat ", " errored)
else "General Nix build failure (check dependencies or environment)."
in
if verbose > 0 then (
if errored <> [] then
print_failed_node_logs drv_paths errored
else
(* Fallback for general Nix failures that were not attributed to a
specific node while streaming the build output. *)
let output = String.trim (Buffer.contents captured_output) in
if output <> "" then
Printf.eprintf "\n--- nix-build failure output ---\n%s\n%!" output
);
Printf.eprintf "\n✖ Pipeline build failed [%s]\n%!" error_summary;
Error (Printf.sprintf "nix-build failed. See details above."))
| Error msg ->
Error (Printf.sprintf "Failed to run nix-build: %s" msg)
end