Skip to content

Commit 7d08e4f

Browse files
committed
Rewrite the image builder in Golang
1 parent af213d6 commit 7d08e4f

File tree

4 files changed

+353
-0
lines changed

4 files changed

+353
-0
lines changed
File renamed without changes.

builder/go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module github.com/sprat/claylinux/builder
2+
3+
go 1.24
4+
5+
require golang.org/x/sys v0.33.0

builder/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
2+
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

builder/main.go

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"flag"
7+
"fmt"
8+
"log"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"strconv"
13+
"strings"
14+
15+
"golang.org/x/sys/unix"
16+
)
17+
18+
// --- Helper functions ---
19+
20+
func die(msg string) {
21+
log.Fatalf("Error: %s", msg)
22+
}
23+
24+
func getSize(path string) int64 {
25+
info, err := os.Stat(path)
26+
if err != nil {
27+
die(fmt.Sprintf("could not stat file %s: %v", path, err))
28+
}
29+
return info.Size()
30+
}
31+
32+
func inMib(n int64) int64 {
33+
return (n + (1 << 20) - 1) >> 20
34+
}
35+
36+
func getSizeMib(path string) int64 {
37+
return inMib(getSize(path))
38+
}
39+
40+
func align(val, multiple int64) int64 {
41+
return ((val + multiple - 1) / multiple) * multiple
42+
}
43+
44+
func run(name string, args ...string) {
45+
cmd := exec.Command(name, args...)
46+
cmd.Stdout = os.Stdout
47+
cmd.Stderr = os.Stderr
48+
if err := cmd.Run(); err != nil {
49+
die(fmt.Sprintf("command failed: %s %v: %v", name, args, err))
50+
}
51+
}
52+
53+
func runOutput(name string, args ...string) string {
54+
cmd := exec.Command(name, args...)
55+
out, err := cmd.Output()
56+
if err != nil {
57+
die(fmt.Sprintf("command failed: %s %v: %v", name, args, err))
58+
}
59+
return strings.TrimSpace(string(out))
60+
}
61+
62+
func fileExists(path string) bool {
63+
_, err := os.Stat(path)
64+
return err == nil
65+
}
66+
67+
// --- Core build steps ---
68+
69+
var (
70+
output string
71+
format string
72+
volume string
73+
compression string
74+
efiArch string
75+
efiStub string
76+
buildDir string
77+
efiFile string
78+
espFile string
79+
)
80+
81+
func getEfiArch() string {
82+
// TODO: use runtime.GOARCH ?
83+
var utsname unix.Utsname
84+
unix.Uname(&utsname)
85+
86+
machineArch := string(utsname.Machine[:])
87+
switch machineArch {
88+
case "aarch64":
89+
return "aa64"
90+
case "arm":
91+
return "arm"
92+
case "i686":
93+
return "ia32"
94+
case "x86_64":
95+
return "x64"
96+
default:
97+
die("unsupported architecture: " + machineArch)
98+
return ""
99+
}
100+
}
101+
102+
func compress(filename string) {
103+
switch compression {
104+
case "none":
105+
// do nothing
106+
case "gz":
107+
run("pigz", "-9", filename)
108+
run("mv", filename+".gz", filename)
109+
case "xz":
110+
run("xz", "-C", "crc32", "-9", "-T0", filename)
111+
run("mv", filename+".xz", filename)
112+
case "zstd":
113+
run("zstd", "-19", "-T0", "--rm", filename)
114+
run("mv", filename+".zstd", filename)
115+
default:
116+
die("invalid compression scheme: " + compression)
117+
}
118+
}
119+
120+
// --- Build steps ---
121+
122+
func buildInitramfs() {
123+
os.Mkdir("initramfs_files", 0755)
124+
run("cp", "/usr/share/claylinux/init", "initramfs_files")
125+
126+
if fileExists("/system/etc/hosts.target") {
127+
os.MkdirAll("initramfs_files/etc", 0755)
128+
run("cp", "/system/etc/hosts.target", "initramfs_files/etc/hosts")
129+
}
130+
if fileExists("/system/etc/resolv.conf.target") {
131+
os.MkdirAll("initramfs_files/etc", 0755)
132+
run("cp", "/system/etc/resolv.conf.target", "initramfs_files/etc/resolv.conf")
133+
}
134+
// cpio for initramfs_files
135+
run("sh", "-c", `find initramfs_files -mindepth 1 -printf '%P\0' | cpio --quiet -o0H newc -D initramfs_files -F initramfs.img`)
136+
// Add system files except boot, hosts.target, resolv.conf.target
137+
run("sh", "-c", `find /system -path /system/boot -prune -o ! -path /system/init ! -path /system/etc/hosts.target ! -path /system/etc/resolv.conf.target -mindepth 1 -printf '%P\0' | cpio --quiet -o0AH newc -D /system -F initramfs.img`)
138+
compress("initramfs.img")
139+
140+
ucode := runOutput("find", "/system/boot/", "-name", "*-ucode.img")
141+
imgs := "initramfs.img"
142+
if ucode != "" {
143+
imgs = ucode + " " + imgs
144+
}
145+
run("sh", "-c", fmt.Sprintf("cat %s >initramfs", imgs))
146+
run("find", ".", "!", "-name", "initramfs", "-delete")
147+
}
148+
149+
// getInitialOffset computes offset+size from objdump output.
150+
func getInitialOffset(efiStub string) (int64, error) {
151+
cmd := exec.Command("objdump", "-h", "-w", efiStub)
152+
out, err := cmd.Output()
153+
if err != nil {
154+
return 0, err
155+
}
156+
scanner := bufio.NewScanner(bytes.NewReader(out))
157+
var lastFields []string
158+
for scanner.Scan() {
159+
line := scanner.Text()
160+
// Sections have lines that start with space+number or name, skip headers
161+
fields := strings.Fields(line)
162+
if len(fields) >= 5 && strings.HasPrefix(fields[1], "0x") {
163+
lastFields = fields
164+
}
165+
}
166+
if len(lastFields) < 5 {
167+
return 0, fmt.Errorf("failed to parse objdump output")
168+
}
169+
offset, err := strconv.ParseInt(lastFields[4], 0, 64)
170+
if err != nil {
171+
return 0, err
172+
}
173+
size, err := strconv.ParseInt(lastFields[3], 0, 64)
174+
if err != nil {
175+
return 0, err
176+
}
177+
return int64(offset + size), nil
178+
}
179+
180+
func buildUKI() {
181+
const alignment = 4096
182+
efiStub := "path/to/efi_stub" // Set your input stub
183+
efiFile := "path/to/efi_file" // Set your output file
184+
185+
// For example, sections := [][2]string{{".foo", "foo.bin"}, {".bar", "bar.bin"}}
186+
sections := [][2]string{
187+
{".section1", "file1.bin"},
188+
{".section2", "file2.bin"},
189+
// Add more as needed
190+
}
191+
192+
// Step 1: Get initial offset from objdump and align it
193+
offset, err := getInitialOffset(efiStub)
194+
if err != nil {
195+
fmt.Fprintf(os.Stderr, "Error getting initial offset: %v\n", err)
196+
os.Exit(1)
197+
}
198+
offset = align(offset, alignment)
199+
200+
// Step 2: Prepare objcopy arguments
201+
var args []string
202+
for _, pair := range sections {
203+
section, file := pair[0], pair[1]
204+
args = append(args, "--add-section", fmt.Sprintf("%s=%s", section, file))
205+
args = append(args, "--change-section-vma", fmt.Sprintf("%s=0x%X", section, offset))
206+
207+
size, err := getSize(file)
208+
if err != nil {
209+
fmt.Fprintf(os.Stderr, "Error getting size of %s: %v\n", file, err)
210+
os.Exit(1)
211+
}
212+
size = align(size, alignment)
213+
offset += size
214+
}
215+
216+
// Step 3: Run objcopy
217+
args = append(args, efiStub, efiFile)
218+
cmd := exec.Command("objcopy", args...)
219+
cmd.Stdout = os.Stdout
220+
cmd.Stderr = os.Stderr
221+
fmt.Printf("Running: objcopy %s\n", strings.Join(args, " "))
222+
if err := cmd.Run(); err != nil {
223+
fmt.Fprintf(os.Stderr, "objcopy failed: %v\n", err)
224+
os.Exit(1)
225+
}
226+
227+
}
228+
229+
func buildEFI() {
230+
os.Chdir(buildDir)
231+
fmt.Println("Building the EFI executable")
232+
buildInitramfs()
233+
size := getSizeMib("initramfs")
234+
fmt.Printf("The size of the initramfs is: %d MiB\n", size)
235+
kernel := runOutput("find", "/system/boot", "-name", "vmlinu*", "-print")
236+
run("sh", "-c", "tr '\\n' ' ' </system/boot/cmdline >cmdline")
237+
run("sh", "-c", "basename /system/lib/modules/* >kernel-release")
238+
239+
// Compose the UKI section file
240+
ukiStanza := fmt.Sprintf(
241+
".osrel /system/etc/os-release\n.uname kernel-release\n.cmdline cmdline\n.initrd initramfs\n.linux %s\n", kernel)
242+
cmd := exec.Command("build_uki")
243+
cmd.Stdin = strings.NewReader(ukiStanza)
244+
if err := cmd.Run(); err != nil {
245+
die("build_uki failed: " + err.Error())
246+
}
247+
248+
run("find", ".", "!", "-name", "*.efi", "-delete")
249+
}
250+
251+
func generateEFI() {
252+
fmt.Println("Copying the OS files to the output directory")
253+
run("mv", efiFile, output+".efi")
254+
}
255+
256+
func generateESP() {
257+
fmt.Println("Generating the EFI System Partition (ESP)")
258+
size := getSize(efiFile)
259+
size = inMib(size * 102 / 100)
260+
fmt.Printf("The size of the ESP is: %d MiB\n", size)
261+
run("mkfs.vfat", "-n", volume, "-F", "32", "-C", espFile, strconv.FormatInt(size<<10, 10))
262+
run("mmd", "-i", espFile, "::/EFI")
263+
run("mmd", "-i", espFile, "::/EFI/boot")
264+
run("mcopy", "-i", espFile, efiFile, "::/EFI/boot/boot"+efiArch+".efi")
265+
run("rm", efiFile)
266+
}
267+
268+
func generateISO() {
269+
generateESP()
270+
fmt.Println("Generating the ISO image")
271+
run("xorrisofs", "-e", filepath.Base(espFile),
272+
"-no-emul-boot", "-joliet", "-full-iso9660-filenames", "-rational-rock",
273+
"-sysid", "LINUX", "-volid", volume,
274+
"-output", output+".iso", espFile)
275+
run("rm", espFile)
276+
}
277+
278+
func generateRaw() {
279+
generateESP()
280+
fmt.Println("Generating the disk image")
281+
diskFile := output + ".img"
282+
espMiB := getSizeMib(espFile)
283+
diskSize := espMiB + 2
284+
fmt.Printf("The size of the disk image is: %d MiB\n", diskSize)
285+
run("truncate", "-s", fmt.Sprintf("%dM", diskSize), diskFile)
286+
sfdiskCmd := fmt.Sprintf("label: gpt\nfirst-lba: 34\nstart=1MiB size=%dMiB name=\"EFI system partition\" type=uefi\n", espMiB)
287+
cmd := exec.Command("sfdisk", "--quiet", diskFile)
288+
cmd.Stdin = strings.NewReader(sfdiskCmd)
289+
if err := cmd.Run(); err != nil {
290+
die("sfdisk failed: " + err.Error())
291+
}
292+
run("dd", "if="+espFile, "of="+diskFile, "bs=1M", "seek=1", "conv=notrunc", "status=none")
293+
run("rm", espFile)
294+
}
295+
296+
func convertImage(format string) {
297+
generateRaw()
298+
fmt.Printf("Converting the disk image to %s format\n", format)
299+
run("qemu-img", "convert", "-f", "raw", "-O", format, output+".img", output+"."+format)
300+
run("rm", output+".img")
301+
}
302+
303+
// --- Option parsing, main ---
304+
305+
func main() {
306+
output = "/out/claylinux"
307+
format = "raw"
308+
volume = "CLAYLINUX"
309+
compression = "gz"
310+
311+
flag.StringVar(&format, "format", format, "Output format (efi, iso, raw, qcow2, vmdk, vhdx, vdi)")
312+
flag.StringVar(&output, "output", output, "Output image path/name, without extension")
313+
flag.StringVar(&volume, "volume", volume, "Volume label for the boot partition")
314+
flag.StringVar(&compression, "compression", compression, "Compression format for initramfs: none|gz|xz|zstd")
315+
flag.Parse()
316+
317+
if _, err := os.Stat("/system"); err != nil {
318+
die("the /system directory does not exist, please copy/mount your root filesystem here")
319+
}
320+
321+
buildDir = runOutput("mktemp", "-d")
322+
efiArch = getEfiArch()
323+
efiStub = "/usr/lib/systemd/boot/efi/linux" + efiArch + ".efi.stub"
324+
efiFile = filepath.Join(buildDir, "claylinux.efi")
325+
espFile = filepath.Join(buildDir, "claylinux.esp")
326+
327+
os.Chdir(buildDir)
328+
buildEFI()
329+
os.Chdir("/")
330+
331+
os.MkdirAll(filepath.Dir(output), 0755)
332+
switch format {
333+
case "efi":
334+
generateEFI()
335+
case "iso":
336+
generateISO()
337+
case "raw":
338+
generateRaw()
339+
case "qcow2", "vmdk", "vhdx", "vdi":
340+
convertImage(format)
341+
default:
342+
die("invalid format: " + format)
343+
}
344+
345+
os.RemoveAll(buildDir)
346+
}

0 commit comments

Comments
 (0)