···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+open Cmdliner
77+88+let log_src = Logs.Src.create "uniboot"
99+1010+module Log = (val Logs.src_log log_src : Logs.LOG)
1111+1212+(* Build command *)
1313+1414+let build_cmd =
1515+ let output =
1616+ let doc = "Output disk image file path." in
1717+ Arg.(required & opt (some string) None & info [ "o"; "output" ] ~docv:"FILE" ~doc)
1818+ in
1919+ let kernel =
2020+ let doc = "Path to kernel image (e.g., vmlinuz)." in
2121+ Arg.(value & opt (some file) None & info [ "k"; "kernel" ] ~docv:"FILE" ~doc)
2222+ in
2323+ let initramfs =
2424+ let doc = "Paths to include in initramfs (cpio archive)." in
2525+ Arg.(value & opt_all file [] & info [ "i"; "initramfs" ] ~docv:"PATH" ~doc)
2626+ in
2727+ let rootfs =
2828+ let doc = "Paths to include in rootfs (squashfs)." in
2929+ Arg.(value & opt_all file [] & info [ "r"; "rootfs" ] ~docv:"PATH" ~doc)
3030+ in
3131+ let esp_size =
3232+ let doc = "EFI System Partition size in MB." in
3333+ Arg.(value & opt int 64 & info [ "esp-size" ] ~docv:"MB" ~doc)
3434+ in
3535+ let rootfs_size =
3636+ let doc = "Root filesystem partition size in MB." in
3737+ Arg.(value & opt int 256 & info [ "rootfs-size" ] ~docv:"MB" ~doc)
3838+ in
3939+ let sector_size =
4040+ let doc = "Disk sector size (512 or 4096)." in
4141+ Arg.(value & opt int 512 & info [ "sector-size" ] ~docv:"BYTES" ~doc)
4242+ in
4343+ let build output kernel initramfs rootfs esp_size rootfs_size sector_size =
4444+ Log.info (fun m -> m "Building disk image: %s" output);
4545+ let partitions =
4646+ let parts = [] in
4747+ (* Add EFI partition if kernel specified *)
4848+ let parts =
4949+ match kernel with
5050+ | Some k ->
5151+ Log.info (fun m -> m "Adding EFI partition with kernel: %s" k);
5252+ Uniboot.Partition.esp ~size_mb:esp_size ~kernel:k :: parts
5353+ | None -> parts
5454+ in
5555+ (* Add rootfs partition *)
5656+ let parts =
5757+ if initramfs <> [] || rootfs <> [] then (
5858+ Log.info (fun m -> m "Adding rootfs partition (%d MB)" rootfs_size);
5959+ let content =
6060+ if rootfs <> [] then Uniboot.Partition.Squashfs rootfs
6161+ else if initramfs <> [] then Uniboot.Partition.Initramfs initramfs
6262+ else Uniboot.Partition.Empty
6363+ in
6464+ Uniboot.Partition.linux ~name:"rootfs" ~size_mb:rootfs_size ~content
6565+ :: parts)
6666+ else parts
6767+ in
6868+ List.rev parts
6969+ in
7070+ if partitions = [] then (
7171+ Log.err (fun m -> m "No partitions specified. Use --kernel, --initramfs, or --rootfs.");
7272+ `Error (false, "No partitions specified"))
7373+ else
7474+ let config = Uniboot.config ~sector_size partitions in
7575+ match Uniboot.build_file config output with
7676+ | Ok () ->
7777+ Log.app (fun m -> m "Successfully created disk image: %s" output);
7878+ `Ok ()
7979+ | Error msg ->
8080+ Log.err (fun m -> m "Failed to build image: %s" msg);
8181+ `Error (false, msg)
8282+ in
8383+ let doc = "Build a bootable disk image." in
8484+ let info = Cmd.info "build" ~doc in
8585+ Cmd.v info
8686+ Term.(
8787+ ret
8888+ (const build $ output $ kernel $ initramfs $ rootfs $ esp_size
8989+ $ rootfs_size $ sector_size $ Vlog.setup "uniboot"))
9090+9191+(* Inspect command *)
9292+9393+let inspect_cmd =
9494+ let image =
9595+ let doc = "Disk image file to inspect." in
9696+ Arg.(required & pos 0 (some file) None & info [] ~docv:"IMAGE" ~doc)
9797+ in
9898+ let inspect image =
9999+ Log.info (fun m -> m "Inspecting disk image: %s" image);
100100+ let ic = open_in_bin image in
101101+ let buf = Bytes.create 512 in
102102+ (* Skip MBR, read GPT header *)
103103+ let _ = seek_in ic 512 in
104104+ let () = really_input ic buf 0 512 in
105105+ close_in ic;
106106+ match Gpt.of_string (Bytes.to_string buf) ~sector_size:512 with
107107+ | Error msg ->
108108+ Log.err (fun m -> m "Failed to read GPT: %s" msg);
109109+ `Error (false, msg)
110110+ | Ok (`Read_partition_table (lba, num_sectors), k) ->
111111+ let ic = open_in_bin image in
112112+ let table_size = num_sectors * 512 in
113113+ let table_buf = Bytes.create table_size in
114114+ let _ = seek_in ic (Int64.to_int lba * 512) in
115115+ let () = really_input ic table_buf 0 table_size in
116116+ close_in ic;
117117+ (match k table_buf with
118118+ | Error msg ->
119119+ Log.err (fun m -> m "Failed to read partition table: %s" msg);
120120+ `Error (false, msg)
121121+ | Ok gpt ->
122122+ Fmt.pr "GPT Disk Image: %s@." image;
123123+ Fmt.pr " Disk GUID: %a@." Uuidm.pp gpt.Gpt.disk_guid;
124124+ Fmt.pr " Sector size: 512@.";
125125+ Fmt.pr " First usable LBA: %Ld@." gpt.Gpt.first_usable_lba;
126126+ Fmt.pr " Last usable LBA: %Ld@." gpt.Gpt.last_usable_lba;
127127+ Fmt.pr " Partitions:@.";
128128+ List.iteri
129129+ (fun i p ->
130130+ if not (Gpt.Partition.is_zero_partition p) then
131131+ Fmt.pr " %d. %s@. Type: %a@. LBA: %Ld - %Ld@."
132132+ (i + 1) p.Gpt.Partition.name Uuidm.pp p.Gpt.Partition.type_guid
133133+ p.Gpt.Partition.starting_lba p.Gpt.Partition.ending_lba)
134134+ gpt.Gpt.partitions;
135135+ `Ok ())
136136+ in
137137+ let doc = "Inspect a disk image's partition table." in
138138+ let info = Cmd.info "inspect" ~doc in
139139+ Cmd.v info Term.(ret (const inspect $ image $ Vlog.setup "uniboot"))
140140+141141+(* Main command *)
142142+143143+let main_cmd =
144144+ let doc = "Minimal bootable disk image builder" in
145145+ let man =
146146+ [
147147+ `S Manpage.s_description;
148148+ `P
149149+ "$(tname) creates bootable disk images with GPT partition tables, \
150150+ combining kernel, initramfs (cpio), and rootfs (squashfs) into a \
151151+ single image.";
152152+ `S Manpage.s_examples;
153153+ `P "Create a simple bootable image with a kernel:";
154154+ `Pre " $(tname) build -k vmlinuz -o boot.img";
155155+ `P "Create an image with kernel and rootfs:";
156156+ `Pre " $(tname) build -k vmlinuz -r /path/to/rootfs -o boot.img";
157157+ `P "Inspect an existing disk image:";
158158+ `Pre " $(tname) inspect boot.img";
159159+ ]
160160+ in
161161+ let info = Cmd.info "uniboot" ~version:"0.1.0" ~doc ~man in
162162+ Cmd.group info [ build_cmd; inspect_cmd ]
163163+164164+let () = exit (Cmd.eval main_cmd)