Build Linux initramfs cpio archives from OCaml
0
fork

Configure Feed

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

feat: add ocaml-initramfs package, use it in space and uniboot

Extract initramfs building into a standalone package with Dir, File, and
Tree entry types. Replaces duplicated cpio logic in space/lib/build.ml
and uniboot/lib/uniboot.ml.

+245
+1
.ocamlformat
··· 1 + version=0.28.1
+22
dune-project
··· 1 + (lang dune 3.21) 2 + 3 + (name initramfs) 4 + 5 + (generate_opam_files true) 6 + 7 + (license ISC) 8 + (authors "Thomas Gazagnaire <thomas@gazagnaire.org>") 9 + (maintainers "Thomas Gazagnaire <thomas@gazagnaire.org>") 10 + 11 + (source (tangled gazagnaire.org/ocaml-initramfs)) 12 + 13 + (package 14 + (name initramfs) 15 + (synopsis "Build initramfs cpio archives from file and directory entries") 16 + (description 17 + "High-level API for building Linux initramfs images (cpio newc format). 18 + Supports directories, files, and recursive directory trees.") 19 + (depends 20 + (ocaml (>= 5.1)) 21 + (cpio (>= 0.1)) 22 + (alcotest :with-test)))
+34
initramfs.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "Build initramfs cpio archives from file and directory entries" 4 + description: """ 5 + High-level API for building Linux initramfs images (cpio newc format). 6 + Supports directories, files, and recursive directory trees.""" 7 + maintainer: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 8 + authors: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 9 + license: "ISC" 10 + homepage: "https://tangled.org/gazagnaire.org/ocaml-initramfs" 11 + bug-reports: "https://tangled.org/gazagnaire.org/ocaml-initramfs/issues" 12 + depends: [ 13 + "dune" {>= "3.21"} 14 + "ocaml" {>= "5.1"} 15 + "cpio" {>= "0.1"} 16 + "alcotest" {with-test} 17 + "odoc" {with-doc} 18 + ] 19 + build: [ 20 + ["dune" "subst"] {dev} 21 + [ 22 + "dune" 23 + "build" 24 + "-p" 25 + name 26 + "-j" 27 + jobs 28 + "@install" 29 + "@runtest" {with-test} 30 + "@doc" {with-doc} 31 + ] 32 + ] 33 + dev-repo: "git+https://tangled.org/gazagnaire.org/ocaml-initramfs" 34 + x-maintenance-intent: ["(latest)"]
+4
lib/dune
··· 1 + (library 2 + (name initramfs) 3 + (public_name initramfs) 4 + (libraries cpio unix))
+52
lib/initramfs.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + type entry = 7 + | Dir of string 8 + | File of { name : string; path : string } 9 + | Tree of string 10 + 11 + let read_file path = 12 + let ic = open_in_bin path in 13 + let len = in_channel_length ic in 14 + let s = Bytes.create len in 15 + really_input ic s 0 len; 16 + close_in ic; 17 + Bytes.unsafe_to_string s 18 + 19 + let file_perm path = 20 + let st = Unix.stat path in 21 + st.Unix.st_perm 22 + 23 + let rec walk acc prefix dir = 24 + let items = Sys.readdir dir |> Array.to_list in 25 + List.fold_left 26 + (fun acc name -> 27 + let full = Filename.concat dir name in 28 + let cpio_name = Filename.concat prefix name in 29 + if Sys.is_directory full then 30 + let acc = Cpio.directory ~perm:0o755 cpio_name :: acc in 31 + walk acc cpio_name full 32 + else 33 + let data = read_file full in 34 + let perm = file_perm full in 35 + Cpio.regular ~perm ~name:cpio_name data :: acc) 36 + acc items 37 + 38 + let entries_of entry = 39 + match entry with 40 + | Dir name -> [ Cpio.directory ~perm:0o755 name ] 41 + | File { name; path } -> 42 + let data = read_file path in 43 + let perm = file_perm path in 44 + [ Cpio.regular ~perm ~name data ] 45 + | Tree path -> 46 + let base = Filename.basename path in 47 + let acc = [ Cpio.directory ~perm:0o755 base ] in 48 + List.rev (walk acc base path) 49 + 50 + let build entries = 51 + let cpio_entries = List.concat_map entries_of entries in 52 + Cpio.to_string cpio_entries
+24
lib/initramfs.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Build initramfs cpio archives. 7 + 8 + High-level API for creating Linux initramfs images (cpio newc format) from 9 + directories, files, and recursive directory trees. *) 10 + 11 + type entry = 12 + | Dir of string (** Empty directory. *) 13 + | File of { name : string; path : string } 14 + (** File from disk, renamed to [name] in the archive. *) 15 + | Tree of string (** Recursive directory tree. *) 16 + 17 + val build : entry list -> string 18 + (** [build entries] builds a cpio newc archive from [entries]. 19 + 20 + - [Dir name] creates an empty directory with permissions 0o755. 21 + - [File { name; path }] reads a file from [path] and stores it as [name], 22 + preserving the original file permissions. 23 + - [Tree path] recursively walks [path], adding all files and subdirectories 24 + as cpio entries, preserving permissions. *)
+4
test/dune
··· 1 + (test 2 + (name test_initramfs) 3 + (package initramfs) 4 + (libraries initramfs cpio alcotest))
+104
test/test_initramfs.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + let with_temp_file content f = 7 + let path = Filename.temp_file "initramfs-test" "" in 8 + let oc = open_out_bin path in 9 + output_string oc content; 10 + close_out oc; 11 + Fun.protect ~finally:(fun () -> Sys.remove path) (fun () -> f path) 12 + 13 + let with_temp_dir f = 14 + let dir = Filename.temp_dir "initramfs-test" "" in 15 + Fun.protect 16 + ~finally:(fun () -> 17 + let rec rm path = 18 + if Sys.is_directory path then begin 19 + Array.iter 20 + (fun name -> rm (Filename.concat path name)) 21 + (Sys.readdir path); 22 + Unix.rmdir path 23 + end 24 + else Sys.remove path 25 + in 26 + rm dir) 27 + (fun () -> f dir) 28 + 29 + let parse archive = 30 + match Cpio.of_string archive with 31 + | Ok entries -> entries 32 + | Error e -> Alcotest.fail e 33 + 34 + let entry_names entries = List.map (fun e -> e.Cpio.header.name) entries 35 + 36 + let test_dir () = 37 + let archive = Initramfs.build [ Dir "dev"; Dir "proc" ] in 38 + let entries = parse archive in 39 + let names = entry_names entries in 40 + Alcotest.(check (list string)) "dirs" [ "dev"; "proc" ] names; 41 + List.iter 42 + (fun e -> 43 + Alcotest.(check int) "dir type" 0o040000 (e.Cpio.header.mode land 0o170000)) 44 + entries 45 + 46 + let test_file () = 47 + with_temp_file "#!/bin/sh\necho hello\n" (fun path -> 48 + Unix.chmod path 0o755; 49 + let archive = Initramfs.build [ File { name = "init"; path } ] in 50 + let entries = parse archive in 51 + Alcotest.(check int) "one entry" 1 (List.length entries); 52 + let e = List.hd entries in 53 + Alcotest.(check string) "name" "init" e.header.name; 54 + Alcotest.(check string) "data" "#!/bin/sh\necho hello\n" e.data; 55 + let perm = Cpio.permissions e.header in 56 + Alcotest.(check bool) "executable" true (perm land 0o111 <> 0)) 57 + 58 + let test_tree () = 59 + with_temp_dir (fun dir -> 60 + Unix.mkdir (Filename.concat dir "sub") 0o755; 61 + let write path content = 62 + let oc = open_out_bin path in 63 + output_string oc content; 64 + close_out oc 65 + in 66 + write (Filename.concat dir "a.txt") "aaa"; 67 + write (Filename.concat dir "sub/b.txt") "bbb"; 68 + let base = Filename.basename dir in 69 + let archive = Initramfs.build [ Tree dir ] in 70 + let entries = parse archive in 71 + let names = entry_names entries in 72 + Alcotest.(check bool) "has base dir" true (List.mem base names); 73 + Alcotest.(check bool) 74 + "has a.txt" true 75 + (List.mem (Filename.concat base "a.txt") names); 76 + Alcotest.(check bool) 77 + "has sub" true 78 + (List.mem (Filename.concat base "sub") names); 79 + Alcotest.(check bool) 80 + "has sub/b.txt" true 81 + (List.mem (Filename.concat base "sub/b.txt") names)) 82 + 83 + let test_mixed () = 84 + with_temp_file "data" (fun path -> 85 + let archive = 86 + Initramfs.build [ Dir "dev"; File { name = "init"; path }; Dir "proc" ] 87 + in 88 + let entries = parse archive in 89 + let names = entry_names entries in 90 + Alcotest.(check bool) "has dev" true (List.mem "dev" names); 91 + Alcotest.(check bool) "has init" true (List.mem "init" names); 92 + Alcotest.(check bool) "has proc" true (List.mem "proc" names)) 93 + 94 + let () = 95 + Alcotest.run "initramfs" 96 + [ 97 + ( "build", 98 + [ 99 + Alcotest.test_case "dir entries" `Quick test_dir; 100 + Alcotest.test_case "file entry" `Quick test_file; 101 + Alcotest.test_case "tree entry" `Quick test_tree; 102 + Alcotest.test_case "mixed entries" `Quick test_mixed; 103 + ] ); 104 + ]