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
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
(* src/package_manager/nix_generator.ml *)
(* Generate and update flake.nix files from T package dependencies *)

open Package_types

(** Convert a git URL like "https://github.com/user/repo" to a flake input
    like "github:user/repo/tag".
    Supports github.com and gitlab.com URLs. *)
let git_url_to_flake_input (dep : dependency) : (string, string) result =
  let url = dep.git_url in
  let tag = dep.tag in
  (* Strip trailing .git if present *)
  let url = if String.length url > 4 && String.sub url (String.length url - 4) 4 = ".git"
            then String.sub url 0 (String.length url - 4) else url in
  (* Try to parse github.com URLs *)
  let try_prefix prefix scheme =
    let plen = String.length prefix in
    if String.length url >= plen && String.sub url 0 plen = prefix then
      let path = String.sub url plen (String.length url - plen) in
      (* Strip trailing / if present *)
      let path = if String.length path > 0 && path.[String.length path - 1] = '/'
                 then String.sub path 0 (String.length path - 1) else path in
      Some (Printf.sprintf "%s:%s/%s" scheme path tag)
    else None
  in
  match try_prefix "https://github.com/" "github" with
  | Some input -> Ok input
  | None ->
  match try_prefix "https://gitlab.com/" "gitlab" with
  | Some input -> Ok input
  | None ->
    (* For other URLs, use git+url *)
    Ok (Printf.sprintf "git+%s?ref=%s" url tag)

(** Nix-safe identifier: replace hyphens with hyphens (they're valid in Nix),
    but ensure it doesn't start with a digit *)
let nix_safe_name name =
  if String.length name > 0 && name.[0] >= '0' && name.[0] <= '9'
  then "_" ^ name
  else name

(** Validate that a string is a safe Nix package identifier.
    Allowed characters: alphanumeric, hyphen, underscore, dot, plus.
    Must not be empty and must not start with a digit or hyphen.
    Leading hyphens are rejected because no Nix package attribute name starts
    with a hyphen, and allowing them could allow injection via e.g. "--eval".
    This prevents injection of arbitrary Nix code from user-controlled TOML fields. *)
let is_valid_nix_pkg_name s =
  let n = String.length s in
  if n = 0 then false
  else
    let first = s.[0] in
    if (first >= '0' && first <= '9') || first = '-' then false
    else
      let rec check i =
        if i >= n then true
        else
          let c = s.[i] in
          if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
             (c >= '0' && c <= '9') || c = '-' || c = '_' || c = '.' || c = '+'
          then check (i + 1)
          else false
      in
      check 0

(** Filter a list of package names to only valid Nix identifiers.
    Prints a warning for each rejected entry so users are informed
    when their config contains an invalid or potentially unsafe identifier. *)
let safe_pkg_names ?(warn = true) names =
  List.filter (fun name ->
    if is_valid_nix_pkg_name name then true
    else begin
      if warn then
        Printf.eprintf
          "Warning: skipping invalid Nix package identifier %S (only alphanumeric, -, _, ., + are allowed)\n%!"
          name;
      false
    end
  ) names

(** Generate a complete project flake.nix from dependencies *)
let generate_project_flake
    ~(project_name : string)
    ~(nixpkgs_date : string)
    ~(t_version : string)
    ~(deps : dependency list)
    ?(r_deps : string list = [])
    ?(py_deps : string list = [])
    ?(py_version : string = "python314")
    ?(additional_tools : string list = [])
    ?(latex_pkgs : string list = [])
    ?(warn_invalid_pkg_names : bool = true)
    () : string =
  let additional_tools = safe_pkg_names ~warn:warn_invalid_pkg_names additional_tools in
  let has_quarto = List.mem "quarto" additional_tools in
  let latex_pkgs = safe_pkg_names ~warn:warn_invalid_pkg_names latex_pkgs in
  let buf = Buffer.create 2048 in
  (* Inputs section *)
  let dep_input_names = List.map (fun d -> nix_safe_name d.dep_name) deps in
  let all_output_args =
    ["self"; "nixpkgs"; "flake-utils"; "t-lang"] @ dep_input_names in
  Buffer.add_string buf "{\n";
  Printf.bprintf buf "  description = \"%s — a T data analysis project\";\n\n"
    project_name;
  Buffer.add_string buf "  inputs = {\n";
  Printf.bprintf buf "    nixpkgs.url = \"github:rstats-on-nix/nixpkgs/%s\";\n"
    nixpkgs_date;
  Buffer.add_string buf "    flake-utils.url = \"github:numtide/flake-utils\";\n";
  let tlang_url = match Sys.getenv_opt "TLANG_FLAKE_URL" with Some url -> url | None -> Printf.sprintf "github:b-rodrigues/tlang/v%s" t_version in Printf.bprintf buf "    t-lang.url = \"%s\";\n" tlang_url;
  if deps <> [] then begin
    Buffer.add_string buf "\n";
    Buffer.add_string buf "    # T packages — synced from tproject.toml by 't update'\n";
    List.iter (fun dep ->
      match git_url_to_flake_input dep with
      | Ok input ->
        Printf.bprintf buf "    %s.url = \"%s\";\n"
          (nix_safe_name dep.dep_name) input
      | Error _ -> ()
    ) deps
  end;
  Buffer.add_string buf "  };\n\n";
  (* nixConfig *)
  Buffer.add_string buf "  nixConfig = {\n";
  Buffer.add_string buf "    extra-substituters = [\n";
  Buffer.add_string buf "      \"https://rstats-on-nix.cachix.org\"\n";
  Buffer.add_string buf "    ];\n";
  Buffer.add_string buf "    extra-trusted-public-keys = [\n";
  Buffer.add_string buf "      \"rstats-on-nix.cachix.org-1:vdiiVgocg6WeJrODIqdprZRUrhi1JzhBnXv7aWI6+F0=\"\n";
  Buffer.add_string buf "    ];\n";
  Buffer.add_string buf "  };\n\n";
  (* Outputs *)
  Printf.bprintf buf "  outputs = { %s }:\n"
    (String.concat ", " all_output_args);
  Buffer.add_string buf "    flake-utils.lib.eachDefaultSystem (system:\n";
  Buffer.add_string buf "      let\n";
  Buffer.add_string buf "        pkgs = nixpkgs.legacyPackages.${system};\n";
  if deps <> [] then begin
    Buffer.add_string buf "\n";
    Buffer.add_string buf "        # T package dependencies (from tproject.toml)\n";
    Buffer.add_string buf "        tPackages = [\n";
    List.iter (fun dep ->
      Printf.bprintf buf "          %s.packages.${system}.default\n"
        (nix_safe_name dep.dep_name)
    ) deps;
    Buffer.add_string buf "        ];\n"
  end;
  if r_deps <> [] then begin
    Buffer.add_string buf "\n";
    Buffer.add_string buf "        # R environment\n";
    Buffer.add_string buf "        r-env = pkgs.rWrapper.override {\n";
    Buffer.add_string buf "          packages = with pkgs.rPackages; [\n";
    List.iter (fun dep ->
      Printf.bprintf buf "            %s\n" dep
    ) r_deps;
    Buffer.add_string buf "          ];\n";
    Buffer.add_string buf "        };\n"
  end;
  if py_deps <> [] then begin
    Buffer.add_string buf "\n";
    Buffer.add_string buf "        # Python environment\n";
    Printf.bprintf buf "        py-env = pkgs.%s.withPackages (python-pkgs: with python-pkgs; [\n" py_version;
    List.iter (fun dep ->
      Printf.bprintf buf "          %s\n" dep
    ) py_deps;
    Buffer.add_string buf "        ]);\n";
  end;
  if additional_tools <> [] then begin
    Buffer.add_string buf "\n";
    Buffer.add_string buf "        # Additional Tools\n";
    Buffer.add_string buf "        additionalTools = with pkgs; [\n";
    List.iter (fun t -> Printf.bprintf buf "          %s\n" t) additional_tools;
    Buffer.add_string buf "        ];\n"
  end;
  if latex_pkgs <> [] then begin
    Buffer.add_string buf "\n";
    Buffer.add_string buf "        # LaTeX Environment\n";
    Buffer.add_string buf "        latex-env = pkgs.texlive.combine {\n";
    Buffer.add_string buf "          inherit (pkgs.texlive) scheme-small;\n";
    List.iter (fun p -> Printf.bprintf buf "          inherit (pkgs.texlive) %s;\n" p) latex_pkgs;
    Buffer.add_string buf "        };\n"
  end;
  Buffer.add_string buf "      in\n";
  Buffer.add_string buf "      {\n";
  Buffer.add_string buf "        devShells.default = pkgs.mkShell {\n";
  Buffer.add_string buf "          buildInputs = [\n";
  Buffer.add_string buf "            t-lang.packages.${system}.default\n";
  if r_deps <> [] then Buffer.add_string buf "            r-env\n";
  if py_deps <> [] then Buffer.add_string buf "            py-env\n";
  if latex_pkgs <> [] then Buffer.add_string buf "            latex-env\n";
  let extra_pkgs = 
    (if additional_tools <> [] then " ++ additionalTools" else "") ^
    (if deps <> [] then " ++ tPackages" else "")
  in
  Buffer.add_string buf (Printf.sprintf "          ]%s;\n" extra_pkgs);
  Buffer.add_string buf "\n";
  Buffer.add_string buf "          shellHook = ''\n";
  if deps <> [] then begin
    Buffer.add_string buf "            export T_PACKAGE_PATH=\"";
    List.iteri (fun i dep ->
      if i > 0 then Buffer.add_char buf ':';
      Printf.bprintf buf "${%s.packages.${system}.default}/lib/t/packages" (nix_safe_name dep.dep_name)
    ) deps;
    Buffer.add_string buf ":''${T_PACKAGE_PATH:-}\"\n"
  end;
  Printf.bprintf buf "            echo \"==================================================\"\n";
  Printf.bprintf buf "            echo \"T Project: %s\"\n" project_name;
  Printf.bprintf buf "            echo \"==================================================\"\n";
  Buffer.add_string buf "            echo \"\"\n";
  Buffer.add_string buf "            echo \"Available commands:\"\n";
  Buffer.add_string buf "            echo \"  t repl              - Start T REPL\"\n";
  Buffer.add_string buf "            echo \"  t run <file>        - Run a T file\"\n";
  Buffer.add_string buf "            echo \"  t test              - Run tests\"\n";
  Buffer.add_string buf "            echo \"\"\n";
  Buffer.add_string buf "            echo \"To add dependencies:\"\n";
  Buffer.add_string buf "            echo \"  * Add them to tproject.toml\"\n";
  Buffer.add_string buf "            echo \"  * Run 't update' to sync flake.nix\"\n";
  Buffer.add_string buf "            echo \"\"\n";
  if has_quarto then begin
    Buffer.add_string buf "            mkdir -p _extensions\n";
    Buffer.add_string buf "            expected_quarto_ext=\"${t-lang.packages.${system}.default}/share/tlang/quarto/tlang\"\n";
    Buffer.add_string buf "            quarto_ext_path=\"_extensions/tlang\"\n";
    Buffer.add_string buf "            quarto_ext_stamp=\"$quarto_ext_path/.tlang-store-path\"\n";
    Buffer.add_string buf "            provision_quarto_ext() {\n";
    Buffer.add_string buf "              rm -rf \"$quarto_ext_path\"\n";
    Buffer.add_string buf "              mkdir -p \"$quarto_ext_path\"\n";
    Buffer.add_string buf "              cp -R \"$expected_quarto_ext\"/. \"$quarto_ext_path\"/\n";
    Buffer.add_string buf "              printf '%s\\n' \"$expected_quarto_ext\" > \"$quarto_ext_stamp\"\n";
    Buffer.add_string buf "              echo \"Provisioned T Quarto extension at _extensions/tlang\"\n";
    Buffer.add_string buf "            }\n";
    Buffer.add_string buf "            if [ -L \"$quarto_ext_path\" ]; then\n";
    Buffer.add_string buf "              provision_quarto_ext\n";
    Buffer.add_string buf "            elif [ -d \"$quarto_ext_path\" ] && [ -f \"$quarto_ext_stamp\" ]; then\n";
    Buffer.add_string buf "              current_quarto_ext=\"$(cat \"$quarto_ext_stamp\")\"\n";
    Buffer.add_string buf "              if [ \"$current_quarto_ext\" != \"$expected_quarto_ext\" ]; then\n";
    Buffer.add_string buf "                provision_quarto_ext\n";
    Buffer.add_string buf "              fi\n";
    Buffer.add_string buf "            elif [ -e \"$quarto_ext_path\" ]; then\n";
    Buffer.add_string buf "              echo \"Quarto extension path _extensions/tlang already exists; leaving it unchanged.\"\n";
    Buffer.add_string buf "            else\n";
    Buffer.add_string buf "              provision_quarto_ext\n";
    Buffer.add_string buf "            fi\n";
    Buffer.add_string buf "            echo \"Quarto is enabled via [additional-tools]. Render {t} chunks with filters: [tlang].\"\n";
    Buffer.add_string buf "            echo \"\"\n";
  end;
  if List.mem "jpmml-evaluator" additional_tools then begin
    Buffer.add_string buf "            export T_JPMML_EVALUATOR_JAR=\"${pkgs.jpmml-evaluator}/share/java/jpmml-evaluator.jar\"\n";
  end;
  if List.mem "jpmml-statsmodels" additional_tools then begin
    Buffer.add_string buf "            export T_JPMML_STATSMODELS_JAR=\"${pkgs.jpmml-statsmodels}/share/java/jpmml-statsmodels.jar\"\n";
  end;
  Buffer.add_string buf "          '';\n";
  Buffer.add_string buf "        };\n";
  Buffer.add_string buf "      }\n";
  Buffer.add_string buf "    );\n";
  Buffer.add_string buf "}\n";
  Buffer.contents buf

(** Generate a complete package flake.nix from dependencies *)
let generate_package_flake
    ~(package_name : string)
    ~(package_version : string)
    ~(nixpkgs_date : string)
    ~(t_version : string)
    ~(deps : dependency list)
    ?(additional_tools : string list = [])
    ?(latex_pkgs : string list = [])
    ?(warn_invalid_pkg_names : bool = true)
    () : string =
  let additional_tools = safe_pkg_names ~warn:warn_invalid_pkg_names additional_tools in
  let latex_pkgs = safe_pkg_names ~warn:warn_invalid_pkg_names latex_pkgs in
  let buf = Buffer.create 2048 in
  let dep_input_names = List.map (fun d -> nix_safe_name d.dep_name) deps in
  let all_output_args =
    ["self"; "nixpkgs"; "flake-utils"; "t-lang"] @ dep_input_names in
  Buffer.add_string buf "{\n";
  Printf.bprintf buf "  description = \"%s — a T package\";\n\n"
    package_name;
  Buffer.add_string buf "  inputs = {\n";
  Printf.bprintf buf "    nixpkgs.url = \"github:rstats-on-nix/nixpkgs/%s\";\n"
    nixpkgs_date;
  Buffer.add_string buf "    flake-utils.url = \"github:numtide/flake-utils\";\n";
  let tlang_url = match Sys.getenv_opt "TLANG_FLAKE_URL" with Some url -> url | None -> Printf.sprintf "github:b-rodrigues/tlang/v%s" t_version in Printf.bprintf buf "    t-lang.url = \"%s\";\n" tlang_url;
  if deps <> [] then begin
    Buffer.add_string buf "\n";
    Buffer.add_string buf "    # Package dependencies — synced from DESCRIPTION.toml by 't update'\n";
    List.iter (fun dep ->
      match git_url_to_flake_input dep with
      | Ok input ->
        Printf.bprintf buf "    %s.url = \"%s\";\n"
          (nix_safe_name dep.dep_name) input
      | Error _ -> ()
    ) deps
  end;
  Buffer.add_string buf "  };\n\n";
  (* Outputs *)
  Printf.bprintf buf "  outputs = { %s }:\n"
    (String.concat ", " all_output_args);
  Buffer.add_string buf "    flake-utils.lib.eachDefaultSystem (system:\n";
  Buffer.add_string buf "      let\n";
  Buffer.add_string buf "        pkgs = nixpkgs.legacyPackages.${system};\n";
  if additional_tools <> [] then begin
    Buffer.add_string buf "\n";
    Buffer.add_string buf "        # Additional Tools\n";
    Buffer.add_string buf "        additionalTools = with pkgs; [\n";
    List.iter (fun t -> Printf.bprintf buf "          %s\n" t) additional_tools;
    Buffer.add_string buf "        ];\n"
  end;
  if latex_pkgs <> [] then begin
    Buffer.add_string buf "\n";
    Buffer.add_string buf "        # LaTeX Environment for documentation\n";
    Buffer.add_string buf "        latex-env = pkgs.texlive.combine {\n";
    Buffer.add_string buf "          inherit (pkgs.texlive) scheme-small;\n";
    List.iter (fun p -> Printf.bprintf buf "          inherit (pkgs.texlive) %s;\n" p) latex_pkgs;
    Buffer.add_string buf "        };\n"
  end;
  Buffer.add_string buf "      in\n";
  Buffer.add_string buf "      {\n";
  (* packages.default *)
  Buffer.add_string buf "        packages.default = pkgs.stdenv.mkDerivation {\n";
  Printf.bprintf buf "          pname = \"t-%s\";\n" package_name;
  Printf.bprintf buf "          version = \"%s\";\n" package_version;
  Buffer.add_string buf "          src = ./.;\n\n";
  Buffer.add_string buf "          buildInputs = [\n";
  List.iter (fun dep ->
    Printf.bprintf buf "            %s.packages.${system}.default\n"
      (nix_safe_name dep.dep_name)
  ) deps;
  Buffer.add_string buf "          ];\n\n";
  Buffer.add_string buf "          installPhase = ''\n";
  Printf.bprintf buf "            mkdir -p $out/lib/t/packages/%s\n" package_name;
  Printf.bprintf buf "            cp -r src $out/lib/t/packages/%s/\n" package_name;
  Buffer.add_string buf "            if [ -d \"help\" ]; then\n";
  Printf.bprintf buf "              cp -r help $out/lib/t/packages/%s/\n" package_name;
  Buffer.add_string buf "            fi\n";
  Buffer.add_string buf "          '';\n\n";
  Buffer.add_string buf "          meta = {\n";
  Printf.bprintf buf "            description = \"%s — a T package\";\n" package_name;
  Buffer.add_string buf "          };\n";
  Buffer.add_string buf "        };\n\n";
  (* devShells.default *)
  Buffer.add_string buf "        devShells.default = pkgs.mkShell {\n";
  Buffer.add_string buf "          buildInputs = [\n";
  Buffer.add_string buf "            t-lang.packages.${system}.default\n";
  if latex_pkgs <> [] then Buffer.add_string buf "            latex-env\n";
  List.iter (fun dep ->
    Printf.bprintf buf "            %s.packages.${system}.default\n"
      (nix_safe_name dep.dep_name)
  ) deps;
  if additional_tools <> [] then
    Buffer.add_string buf "          ] ++ additionalTools;\n"
  else
    Buffer.add_string buf "          ];\n";
  Buffer.add_string buf "\n";
  Buffer.add_string buf "          shellHook = ''\n";
  if deps <> [] then begin
    Buffer.add_string buf "            export T_PACKAGE_PATH=\"";
    List.iteri (fun i dep ->
      if i > 0 then Buffer.add_char buf ':';
      Printf.bprintf buf "${%s.packages.${system}.default}/lib/t/packages" (nix_safe_name dep.dep_name)
    ) deps;
    Buffer.add_string buf ":''${T_PACKAGE_PATH:-}\"\n"
  end;
  Printf.bprintf buf "            echo \"==================================================\"\n";
  Printf.bprintf buf "            echo \"T Package: %s\"\n" package_name;
  Printf.bprintf buf "            echo \"==================================================\"\n";
  Buffer.add_string buf "            echo \"\"\n";
  Buffer.add_string buf "            echo \"Available commands:\"\n";
  Buffer.add_string buf "            echo \"  t repl              - Start T REPL\"\n";
  Buffer.add_string buf "            echo \"  t run <file>        - Run a T file\"\n";
  Buffer.add_string buf "            echo \"  t test              - Run tests\"\n";
  Buffer.add_string buf "            echo \"\"\n";
  Buffer.add_string buf "            echo \"To add dependencies:\"\n";
  Buffer.add_string buf "            echo \"  * Add them to DESCRIPTION.toml\"\n";
  Buffer.add_string buf "            echo \"  * Run 't update' to sync flake.nix\"\n";
  Buffer.add_string buf "            echo \"\"\n";
  if List.mem "jpmml-evaluator" additional_tools then begin
    Buffer.add_string buf "            export T_JPMML_EVALUATOR_JAR=\"${pkgs.jpmml-evaluator}/share/java/jpmml-evaluator.jar\"\n";
  end;
  if List.mem "jpmml-statsmodels" additional_tools then begin
    Buffer.add_string buf "            export T_JPMML_STATSMODELS_JAR=\"${pkgs.jpmml-statsmodels}/share/java/jpmml-statsmodels.jar\"\n";
  end;
  Buffer.add_string buf "          '';\n";
  Buffer.add_string buf "        };\n";
  Buffer.add_string buf "      }\n";
  Buffer.add_string buf "    );\n";
  Buffer.add_string buf "}\n";
  Buffer.contents buf

(** Update a flake.nix file in-place or create a new one.
    Backs up the original to flake.nix.bak if it exists.
    [kind] is either `Project` or `Package`. *)
type flake_kind = Project | Package

let install_flake
    ~(kind : flake_kind)
    ~(name : string)
    ~(version : string)
    ~(nixpkgs_date : string)
    ~(t_version : string)
    ~(deps : dependency list)
    ?(r_deps : string list = [])
    ?(py_deps : string list = [])
    ?(py_version : string = "python314")
    ?(additional_tools : string list = [])
    ?(latex_pkgs : string list = [])
    ~(dir : string)
    ~(dry_run : bool)
    () : (string, string) result =
  let flake_path = Filename.concat dir "flake.nix" in
  let content = match kind with
    | Project ->
      generate_project_flake ~project_name:name ~nixpkgs_date ~t_version ~deps ~r_deps ~py_deps ~py_version ~additional_tools ~latex_pkgs ()
    | Package ->
      generate_package_flake ~package_name:name ~package_version:version
        ~nixpkgs_date ~t_version ~deps ~additional_tools ~latex_pkgs ()
  in
  if dry_run then begin
    Printf.printf "=== Dry run: flake.nix would be written to %s ===\n\n" flake_path;
    Printf.printf "%s\n" content;
    Ok content
  end else begin
    (* Backup existing flake.nix *)
    (if Sys.file_exists flake_path then begin
      let bak = flake_path ^ ".bak" in
      let ch = open_in flake_path in
      let old = really_input_string ch (in_channel_length ch) in
      close_in ch;
      let ch_out = open_out bak in
      output_string ch_out old;
      close_out ch_out
    end);
    (* Write new flake.nix *)
    let ch = open_out flake_path in
    output_string ch content;
    close_out ch;
    Ok content
  end