Minimal bootable disk image builder
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Initial uniboot implementation

+633
+17
.gitignore
··· 1 + # OCaml build artifacts 2 + _build/ 3 + *.install 4 + *.merlin 5 + 6 + # Dune package management 7 + dune.lock/ 8 + 9 + # Editor and OS files 10 + .DS_Store 11 + *.swp 12 + *~ 13 + .vscode/ 14 + .idea/ 15 + 16 + # Opam local switch 17 + _opam/
+1
.ocamlformat
··· 1 + version = 0.28.1
+4
bin/dune
··· 1 + (executable 2 + (name main) 3 + (public_name uniboot) 4 + (libraries uniboot cmdliner vlog fmt))
+164
bin/main.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + open Cmdliner 7 + 8 + let log_src = Logs.Src.create "uniboot" 9 + 10 + module Log = (val Logs.src_log log_src : Logs.LOG) 11 + 12 + (* Build command *) 13 + 14 + let build_cmd = 15 + let output = 16 + let doc = "Output disk image file path." in 17 + Arg.(required & opt (some string) None & info [ "o"; "output" ] ~docv:"FILE" ~doc) 18 + in 19 + let kernel = 20 + let doc = "Path to kernel image (e.g., vmlinuz)." in 21 + Arg.(value & opt (some file) None & info [ "k"; "kernel" ] ~docv:"FILE" ~doc) 22 + in 23 + let initramfs = 24 + let doc = "Paths to include in initramfs (cpio archive)." in 25 + Arg.(value & opt_all file [] & info [ "i"; "initramfs" ] ~docv:"PATH" ~doc) 26 + in 27 + let rootfs = 28 + let doc = "Paths to include in rootfs (squashfs)." in 29 + Arg.(value & opt_all file [] & info [ "r"; "rootfs" ] ~docv:"PATH" ~doc) 30 + in 31 + let esp_size = 32 + let doc = "EFI System Partition size in MB." in 33 + Arg.(value & opt int 64 & info [ "esp-size" ] ~docv:"MB" ~doc) 34 + in 35 + let rootfs_size = 36 + let doc = "Root filesystem partition size in MB." in 37 + Arg.(value & opt int 256 & info [ "rootfs-size" ] ~docv:"MB" ~doc) 38 + in 39 + let sector_size = 40 + let doc = "Disk sector size (512 or 4096)." in 41 + Arg.(value & opt int 512 & info [ "sector-size" ] ~docv:"BYTES" ~doc) 42 + in 43 + let build output kernel initramfs rootfs esp_size rootfs_size sector_size = 44 + Log.info (fun m -> m "Building disk image: %s" output); 45 + let partitions = 46 + let parts = [] in 47 + (* Add EFI partition if kernel specified *) 48 + let parts = 49 + match kernel with 50 + | Some k -> 51 + Log.info (fun m -> m "Adding EFI partition with kernel: %s" k); 52 + Uniboot.Partition.esp ~size_mb:esp_size ~kernel:k :: parts 53 + | None -> parts 54 + in 55 + (* Add rootfs partition *) 56 + let parts = 57 + if initramfs <> [] || rootfs <> [] then ( 58 + Log.info (fun m -> m "Adding rootfs partition (%d MB)" rootfs_size); 59 + let content = 60 + if rootfs <> [] then Uniboot.Partition.Squashfs rootfs 61 + else if initramfs <> [] then Uniboot.Partition.Initramfs initramfs 62 + else Uniboot.Partition.Empty 63 + in 64 + Uniboot.Partition.linux ~name:"rootfs" ~size_mb:rootfs_size ~content 65 + :: parts) 66 + else parts 67 + in 68 + List.rev parts 69 + in 70 + if partitions = [] then ( 71 + Log.err (fun m -> m "No partitions specified. Use --kernel, --initramfs, or --rootfs."); 72 + `Error (false, "No partitions specified")) 73 + else 74 + let config = Uniboot.config ~sector_size partitions in 75 + match Uniboot.build_file config output with 76 + | Ok () -> 77 + Log.app (fun m -> m "Successfully created disk image: %s" output); 78 + `Ok () 79 + | Error msg -> 80 + Log.err (fun m -> m "Failed to build image: %s" msg); 81 + `Error (false, msg) 82 + in 83 + let doc = "Build a bootable disk image." in 84 + let info = Cmd.info "build" ~doc in 85 + Cmd.v info 86 + Term.( 87 + ret 88 + (const build $ output $ kernel $ initramfs $ rootfs $ esp_size 89 + $ rootfs_size $ sector_size $ Vlog.setup "uniboot")) 90 + 91 + (* Inspect command *) 92 + 93 + let inspect_cmd = 94 + let image = 95 + let doc = "Disk image file to inspect." in 96 + Arg.(required & pos 0 (some file) None & info [] ~docv:"IMAGE" ~doc) 97 + in 98 + let inspect image = 99 + Log.info (fun m -> m "Inspecting disk image: %s" image); 100 + let ic = open_in_bin image in 101 + let buf = Bytes.create 512 in 102 + (* Skip MBR, read GPT header *) 103 + let _ = seek_in ic 512 in 104 + let () = really_input ic buf 0 512 in 105 + close_in ic; 106 + match Gpt.of_string (Bytes.to_string buf) ~sector_size:512 with 107 + | Error msg -> 108 + Log.err (fun m -> m "Failed to read GPT: %s" msg); 109 + `Error (false, msg) 110 + | Ok (`Read_partition_table (lba, num_sectors), k) -> 111 + let ic = open_in_bin image in 112 + let table_size = num_sectors * 512 in 113 + let table_buf = Bytes.create table_size in 114 + let _ = seek_in ic (Int64.to_int lba * 512) in 115 + let () = really_input ic table_buf 0 table_size in 116 + close_in ic; 117 + (match k table_buf with 118 + | Error msg -> 119 + Log.err (fun m -> m "Failed to read partition table: %s" msg); 120 + `Error (false, msg) 121 + | Ok gpt -> 122 + Fmt.pr "GPT Disk Image: %s@." image; 123 + Fmt.pr " Disk GUID: %a@." Uuidm.pp gpt.Gpt.disk_guid; 124 + Fmt.pr " Sector size: 512@."; 125 + Fmt.pr " First usable LBA: %Ld@." gpt.Gpt.first_usable_lba; 126 + Fmt.pr " Last usable LBA: %Ld@." gpt.Gpt.last_usable_lba; 127 + Fmt.pr " Partitions:@."; 128 + List.iteri 129 + (fun i p -> 130 + if not (Gpt.Partition.is_zero_partition p) then 131 + Fmt.pr " %d. %s@. Type: %a@. LBA: %Ld - %Ld@." 132 + (i + 1) p.Gpt.Partition.name Uuidm.pp p.Gpt.Partition.type_guid 133 + p.Gpt.Partition.starting_lba p.Gpt.Partition.ending_lba) 134 + gpt.Gpt.partitions; 135 + `Ok ()) 136 + in 137 + let doc = "Inspect a disk image's partition table." in 138 + let info = Cmd.info "inspect" ~doc in 139 + Cmd.v info Term.(ret (const inspect $ image $ Vlog.setup "uniboot")) 140 + 141 + (* Main command *) 142 + 143 + let main_cmd = 144 + let doc = "Minimal bootable disk image builder" in 145 + let man = 146 + [ 147 + `S Manpage.s_description; 148 + `P 149 + "$(tname) creates bootable disk images with GPT partition tables, \ 150 + combining kernel, initramfs (cpio), and rootfs (squashfs) into a \ 151 + single image."; 152 + `S Manpage.s_examples; 153 + `P "Create a simple bootable image with a kernel:"; 154 + `Pre " $(tname) build -k vmlinuz -o boot.img"; 155 + `P "Create an image with kernel and rootfs:"; 156 + `Pre " $(tname) build -k vmlinuz -r /path/to/rootfs -o boot.img"; 157 + `P "Inspect an existing disk image:"; 158 + `Pre " $(tname) inspect boot.img"; 159 + ] 160 + in 161 + let info = Cmd.info "uniboot" ~version:"0.1.0" ~doc ~man in 162 + Cmd.group info [ build_cmd; inspect_cmd ] 163 + 164 + let () = exit (Cmd.eval main_cmd)
+40
dune-project
··· 1 + (lang dune 3.17) 2 + 3 + (name uniboot) 4 + 5 + (generate_opam_files true) 6 + 7 + (license ISC) 8 + 9 + (authors "Thomas Gazagnaire <thomas@gazagnaire.org>") 10 + 11 + (maintainers "Thomas Gazagnaire <thomas@gazagnaire.org>") 12 + 13 + (source 14 + (uri https://tangled.org/gazagnaire.org/ocaml-uniboot)) 15 + 16 + (bug_reports "https://tangled.org/gazagnaire.org/ocaml-uniboot/issues") 17 + 18 + (package 19 + (name uniboot) 20 + (synopsis "Minimal bootable disk image builder") 21 + (description 22 + "Pure OCaml tool for creating bootable disk images with GPT partition tables, 23 + initramfs (cpio), and read-only root filesystems (squashfs). LinuxKit-inspired 24 + workflow for building minimal, reproducible boot images.") 25 + (depends 26 + (ocaml (>= 5.1)) 27 + (bytesrw (>= 0.1)) 28 + (gpt (>= 0.1)) 29 + (mbr-format (>= 0.1)) 30 + (squashfs (>= 0.1)) 31 + (cpio (>= 0.1)) 32 + (tar (>= 3.0)) 33 + (yamlrw (>= 0.1)) 34 + (cmdliner (>= 1.2)) 35 + (fmt (>= 0.9)) 36 + (logs (>= 0.7)) 37 + (vlog (>= 0.1)) 38 + (digestif (>= 1.0)) 39 + (uuidm (>= 0.9.7)) 40 + (alcotest :with-test)))
+15
lib/dune
··· 1 + (library 2 + (name uniboot) 3 + (public_name uniboot) 4 + (libraries 5 + bytesrw 6 + gpt 7 + mbr-format 8 + squashfs 9 + cpio 10 + tar 11 + yamlrw 12 + fmt 13 + logs 14 + digestif 15 + uuidm))
+231
lib/uniboot.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + module Log = (val Logs.src_log (Logs.Src.create "uniboot") : Logs.LOG) 7 + module Writer = Bytesrw.Bytes.Writer 8 + module Reader = Bytesrw.Bytes.Reader 9 + module Slice = Bytesrw.Bytes.Slice 10 + 11 + let ( let* ) = Result.bind 12 + 13 + (* Well-known partition type GUIDs *) 14 + let guid_efi_system = 15 + Option.get (Uuidm.of_string "C12A7328-F81F-11D2-BA4B-00A0C93EC93B") 16 + 17 + let guid_linux_filesystem = 18 + Option.get (Uuidm.of_string "0FC63DAF-8483-4772-8E79-3D69D8477DE4") 19 + 20 + let guid_linux_swap = 21 + Option.get (Uuidm.of_string "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F") 22 + 23 + module Partition = struct 24 + type content = 25 + | Kernel of string 26 + | Initramfs of string list 27 + | Squashfs of string list 28 + | Raw of string 29 + | Empty 30 + 31 + type t = { 32 + name : string; 33 + type_guid : Uuidm.t; 34 + size_mb : int; 35 + content : content; 36 + } 37 + 38 + let utf16le_name name = 39 + (* Convert ASCII to UTF-16LE, padded to 72 bytes (36 chars) *) 40 + let max_chars = 36 in 41 + let len = min (String.length name) max_chars in 42 + let buf = Bytes.make 72 '\000' in 43 + for i = 0 to len - 1 do 44 + Bytes.set buf (i * 2) name.[i] 45 + done; 46 + Bytes.to_string buf 47 + 48 + let esp ~size_mb ~kernel = 49 + { 50 + name = utf16le_name "EFI System"; 51 + type_guid = guid_efi_system; 52 + size_mb; 53 + content = Kernel kernel; 54 + } 55 + 56 + let linux ~name ~size_mb ~content = 57 + { 58 + name = utf16le_name name; 59 + type_guid = guid_linux_filesystem; 60 + size_mb; 61 + content; 62 + } 63 + 64 + let swap ~size_mb = 65 + { 66 + name = utf16le_name "swap"; 67 + type_guid = guid_linux_swap; 68 + size_mb; 69 + content = Empty; 70 + } 71 + end 72 + 73 + type config = { 74 + sector_size : int; 75 + partitions : Partition.t list; 76 + } 77 + 78 + let config ?(sector_size = 512) partitions = { sector_size; partitions } 79 + 80 + (* Calculate total disk size needed *) 81 + let calculate_disk_size config = 82 + let sector_size = config.sector_size in 83 + (* GPT needs: 1 sector for protective MBR, 1 for GPT header, 32 for partition table *) 84 + let gpt_overhead_sectors = 34 in 85 + (* Same at the end for backup GPT *) 86 + let backup_gpt_sectors = 33 in 87 + let partition_sectors = 88 + List.fold_left 89 + (fun acc p -> 90 + let size_bytes = p.Partition.size_mb * 1024 * 1024 in 91 + acc + ((size_bytes + sector_size - 1) / sector_size)) 92 + 0 config.partitions 93 + in 94 + let total_sectors = 95 + gpt_overhead_sectors + partition_sectors + backup_gpt_sectors 96 + in 97 + Int64.of_int total_sectors 98 + 99 + (* Write zeros to fill a partition *) 100 + let write_zeros writer count = 101 + let chunk_size = 4096 in 102 + let zeros = String.make chunk_size '\000' in 103 + let rec loop remaining = 104 + if remaining <= 0 then () 105 + else 106 + let to_write = min remaining chunk_size in 107 + let slice = 108 + if to_write = chunk_size then Slice.of_string zeros 109 + else Slice.of_string (String.sub zeros 0 to_write) 110 + in 111 + Writer.write writer slice; 112 + loop (remaining - to_write) 113 + in 114 + loop count 115 + 116 + (* Write a file's contents *) 117 + let write_file writer path size_bytes = 118 + let ic = open_in_bin path in 119 + let chunk_size = 4096 in 120 + let buf = Bytes.create chunk_size in 121 + let rec loop written = 122 + if written >= size_bytes then () 123 + else 124 + let to_read = min chunk_size (size_bytes - written) in 125 + let len = input ic buf 0 to_read in 126 + if len = 0 then ( 127 + (* Pad with zeros if file is smaller than partition *) 128 + write_zeros writer (size_bytes - written)) 129 + else ( 130 + Writer.write writer (Slice.of_bytes buf ~first:0 ~length:len); 131 + loop (written + len)) 132 + in 133 + loop 0; 134 + close_in ic 135 + 136 + (* Build partition content *) 137 + let build_partition_content writer partition size_bytes = 138 + match partition.Partition.content with 139 + | Empty -> write_zeros writer size_bytes 140 + | Raw path -> write_file writer path size_bytes 141 + | Kernel path -> 142 + Log.info (fun m -> m "Writing kernel from %s" path); 143 + write_file writer path size_bytes 144 + | Initramfs _paths -> 145 + (* TODO: Build cpio archive from paths *) 146 + Log.warn (fun m -> m "Initramfs building not yet implemented"); 147 + write_zeros writer size_bytes 148 + | Squashfs _paths -> 149 + (* TODO: Build squashfs from paths *) 150 + Log.warn (fun m -> m "Squashfs building not yet implemented"); 151 + write_zeros writer size_bytes 152 + 153 + let build config writer = 154 + let sector_size = config.sector_size in 155 + let disk_sectors = calculate_disk_size config in 156 + Log.info (fun m -> 157 + m "Building disk image: %Ld sectors (%Ld bytes)" disk_sectors 158 + (Int64.mul disk_sectors (Int64.of_int sector_size))); 159 + 160 + (* Calculate partition LBAs *) 161 + let first_usable_lba = 34L in 162 + let partitions_with_lba = 163 + let _, parts = 164 + List.fold_left 165 + (fun (lba, acc) p -> 166 + let size_sectors = 167 + Int64.of_int ((p.Partition.size_mb * 1024 * 1024) / sector_size) 168 + in 169 + let end_lba = Int64.sub (Int64.add lba size_sectors) 1L in 170 + (Int64.add end_lba 1L, (p, lba, end_lba) :: acc)) 171 + (first_usable_lba, []) 172 + config.partitions 173 + in 174 + List.rev parts 175 + in 176 + 177 + (* Create GPT partitions *) 178 + let* gpt_partitions = 179 + List.fold_left 180 + (fun acc (p, start_lba, end_lba) -> 181 + let* parts = acc in 182 + let* part = 183 + Gpt.Partition.make ~name:p.Partition.name 184 + ~type_guid:p.Partition.type_guid ~attributes:0L start_lba end_lba 185 + in 186 + Ok (part :: parts)) 187 + (Ok []) partitions_with_lba 188 + |> Result.map List.rev 189 + in 190 + 191 + (* Create GPT *) 192 + let* gpt = Gpt.make ~disk_sectors ~sector_size gpt_partitions in 193 + Log.info (fun m -> m "Created GPT with %d partitions" (List.length gpt_partitions)); 194 + 195 + (* Write protective MBR (sector 0) *) 196 + let mbr = Gpt.protective_mbr ~sector_size gpt in 197 + Mbr.write writer mbr; 198 + Log.debug (fun m -> m "Wrote protective MBR"); 199 + 200 + (* Write primary GPT header (sector 1) *) 201 + Gpt.write_header writer ~sector_size ~primary:true gpt; 202 + Log.debug (fun m -> m "Wrote primary GPT header"); 203 + 204 + (* Write partition table (sectors 2-33) *) 205 + Gpt.write_partition_table writer ~sector_size gpt; 206 + Log.debug (fun m -> m "Wrote partition table"); 207 + 208 + (* Write partition contents *) 209 + List.iter 210 + (fun (p, _start_lba, _end_lba) -> 211 + let size_bytes = p.Partition.size_mb * 1024 * 1024 in 212 + Log.info (fun m -> m "Writing partition %S (%d MB)" p.Partition.name p.Partition.size_mb); 213 + build_partition_content writer p size_bytes) 214 + partitions_with_lba; 215 + 216 + (* Write backup partition table *) 217 + Gpt.write_partition_table writer ~sector_size gpt; 218 + Log.debug (fun m -> m "Wrote backup partition table"); 219 + 220 + (* Write backup GPT header *) 221 + Gpt.write_header writer ~sector_size ~primary:false gpt; 222 + Log.debug (fun m -> m "Wrote backup GPT header"); 223 + 224 + Ok () 225 + 226 + let build_file config path = 227 + let oc = open_out_bin path in 228 + let writer = Writer.of_out_channel oc in 229 + let result = build config writer in 230 + close_out oc; 231 + result
+69
lib/uniboot.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Minimal bootable disk image builder. 7 + 8 + Uniboot creates bootable disk images with GPT partition tables, 9 + combining initramfs (cpio) and rootfs (squashfs) into a single 10 + bootable image. *) 11 + 12 + (** {1 Configuration} *) 13 + 14 + (** Partition specification. *) 15 + module Partition : sig 16 + type content = 17 + | Kernel of string (** Path to kernel image *) 18 + | Initramfs of string list (** Paths to include in cpio initramfs *) 19 + | Squashfs of string list (** Paths to include in squashfs rootfs *) 20 + | Raw of string (** Raw file to copy as partition content *) 21 + | Empty (** Empty partition (zeros) *) 22 + 23 + type t = { 24 + name : string; (** Partition name (max 36 UTF-16 chars) *) 25 + type_guid : Uuidm.t; (** Partition type GUID *) 26 + size_mb : int; (** Size in megabytes *) 27 + content : content; (** Partition content *) 28 + } 29 + 30 + val esp : size_mb:int -> kernel:string -> t 31 + (** [esp ~size_mb ~kernel] creates an EFI System Partition. *) 32 + 33 + val linux : name:string -> size_mb:int -> content:content -> t 34 + (** [linux ~name ~size_mb ~content] creates a Linux partition. *) 35 + 36 + val swap : size_mb:int -> t 37 + (** [swap ~size_mb] creates a swap partition. *) 38 + end 39 + 40 + (** Disk image configuration. *) 41 + type config = { 42 + sector_size : int; (** Sector size, usually 512 *) 43 + partitions : Partition.t list; (** Partition layout *) 44 + } 45 + 46 + val config : ?sector_size:int -> Partition.t list -> config 47 + (** [config ?sector_size partitions] creates a disk configuration. 48 + @param sector_size defaults to 512 *) 49 + 50 + (** {1 Image Building} *) 51 + 52 + val build : config -> Bytesrw.Bytes.Writer.t -> (unit, string) result 53 + (** [build config writer] builds a bootable disk image and writes it 54 + to [writer]. Returns [Error msg] if building fails. *) 55 + 56 + val build_file : config -> string -> (unit, string) result 57 + (** [build_file config path] builds a bootable disk image and writes 58 + it to the file at [path]. *) 59 + 60 + (** {1 Well-known GUIDs} *) 61 + 62 + val guid_efi_system : Uuidm.t 63 + (** EFI System Partition type GUID. *) 64 + 65 + val guid_linux_filesystem : Uuidm.t 66 + (** Linux filesystem partition type GUID. *) 67 + 68 + val guid_linux_swap : Uuidm.t 69 + (** Linux swap partition type GUID. *)
+3
test/dune
··· 1 + (test 2 + (name test_uniboot) 3 + (libraries uniboot alcotest))
+44
test/test_uniboot.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + let test_config () = 7 + let config = Uniboot.config [] in 8 + Alcotest.(check int) "default sector size" 512 config.sector_size 9 + 10 + let test_partition_esp () = 11 + let p = Uniboot.Partition.esp ~size_mb:64 ~kernel:"/boot/vmlinuz" in 12 + Alcotest.(check int) "ESP size" 64 p.size_mb; 13 + Alcotest.(check bool) "ESP type GUID" true 14 + (Uuidm.equal p.type_guid Uniboot.guid_efi_system) 15 + 16 + let test_partition_linux () = 17 + let p = 18 + Uniboot.Partition.linux ~name:"rootfs" ~size_mb:256 19 + ~content:Uniboot.Partition.Empty 20 + in 21 + Alcotest.(check int) "Linux partition size" 256 p.size_mb; 22 + Alcotest.(check bool) "Linux type GUID" true 23 + (Uuidm.equal p.type_guid Uniboot.guid_linux_filesystem) 24 + 25 + let test_partition_swap () = 26 + let p = Uniboot.Partition.swap ~size_mb:128 in 27 + Alcotest.(check int) "Swap size" 128 p.size_mb; 28 + Alcotest.(check bool) "Swap type GUID" true 29 + (Uuidm.equal p.type_guid Uniboot.guid_linux_swap) 30 + 31 + let () = 32 + Alcotest.run "uniboot" 33 + [ 34 + ( "config", 35 + [ 36 + Alcotest.test_case "default config" `Quick test_config; 37 + ] ); 38 + ( "partition", 39 + [ 40 + Alcotest.test_case "ESP partition" `Quick test_partition_esp; 41 + Alcotest.test_case "Linux partition" `Quick test_partition_linux; 42 + Alcotest.test_case "Swap partition" `Quick test_partition_swap; 43 + ] ); 44 + ]
+45
uniboot.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "Minimal bootable disk image builder" 4 + description: """ 5 + Pure OCaml tool for creating bootable disk images with GPT partition tables, 6 + initramfs (cpio), and read-only root filesystems (squashfs). LinuxKit-inspired 7 + workflow for building minimal, reproducible boot images.""" 8 + maintainer: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 9 + authors: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 10 + license: "ISC" 11 + bug-reports: "https://tangled.org/gazagnaire.org/ocaml-uniboot/issues" 12 + depends: [ 13 + "dune" {>= "3.17"} 14 + "ocaml" {>= "5.1"} 15 + "bytesrw" {>= "0.1"} 16 + "gpt" {>= "0.1"} 17 + "mbr-format" {>= "0.1"} 18 + "squashfs" {>= "0.1"} 19 + "cpio" {>= "0.1"} 20 + "tar" {>= "3.0"} 21 + "yamlrw" {>= "0.1"} 22 + "cmdliner" {>= "1.2"} 23 + "fmt" {>= "0.9"} 24 + "logs" {>= "0.7"} 25 + "vlog" {>= "0.1"} 26 + "digestif" {>= "1.0"} 27 + "uuidm" {>= "0.9.7"} 28 + "alcotest" {with-test} 29 + "odoc" {with-doc} 30 + ] 31 + build: [ 32 + ["dune" "subst"] {dev} 33 + [ 34 + "dune" 35 + "build" 36 + "-p" 37 + name 38 + "-j" 39 + jobs 40 + "@install" 41 + "@runtest" {with-test} 42 + "@doc" {with-doc} 43 + ] 44 + ] 45 + dev-repo: "https://tangled.org/gazagnaire.org/ocaml-uniboot"