|
@@ -0,0 +1,362 @@
|
|
|
+package main
|
|
|
+
|
|
|
+import (
|
|
|
+ "flag"
|
|
|
+ "fmt"
|
|
|
+ "github.com/diskfs/go-diskfs/disk"
|
|
|
+ "github.com/diskfs/go-diskfs/filesystem"
|
|
|
+ "github.com/diskfs/go-diskfs/filesystem/fat32"
|
|
|
+ "io"
|
|
|
+ "io/fs"
|
|
|
+ "os"
|
|
|
+ "path/filepath"
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
+
|
|
|
+ ack "git.swzry.com/zry/go-cli-ack"
|
|
|
+ diskfs "github.com/diskfs/go-diskfs"
|
|
|
+ "github.com/diskfs/go-diskfs/partition/mbr"
|
|
|
+ humanize "github.com/dustin/go-humanize"
|
|
|
+ "github.com/liushuochen/gotable"
|
|
|
+ yaml "gopkg.in/yaml.v3"
|
|
|
+)
|
|
|
+
|
|
|
+const (
|
|
|
+ BLOCK_SIZE = 512
|
|
|
+ UBOOT_OFFSET = 8192
|
|
|
+ FIRST_PART_OFFSET = 1024 * 1024
|
|
|
+ LAST_RESERVED = 4 * 1024 * 1024
|
|
|
+)
|
|
|
+
|
|
|
+type ConfigDef struct {
|
|
|
+ UBootImg string `yaml:"u_boot_img"`
|
|
|
+ BootDir string `yaml:"boot_dir"`
|
|
|
+ BootFsSize int64 `yaml:"boot_fs_size"`
|
|
|
+ RootImg string `yaml:"root_img"`
|
|
|
+ UpperImg string `yaml:"upper_img"`
|
|
|
+ SwapSize int64 `yaml:"swap_size"`
|
|
|
+}
|
|
|
+
|
|
|
+var OutputImg string
|
|
|
+var PrintHelp bool
|
|
|
+var ConfigFile string
|
|
|
+var Cfg ConfigDef
|
|
|
+var QuietMode bool
|
|
|
+
|
|
|
+func main() {
|
|
|
+ flag.BoolVar(&PrintHelp, "h", false, "print this help")
|
|
|
+ flag.StringVar(&OutputImg, "o", "sdcard.img", "output image file")
|
|
|
+ flag.StringVar(&ConfigFile, "i", "config.yaml", "input config file")
|
|
|
+ flag.BoolVar(&QuietMode, "q", false, "quiet mode, which does not ask the user whether to continue")
|
|
|
+ flag.Parse()
|
|
|
+ if PrintHelp {
|
|
|
+ flag.PrintDefaults()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ fcfg, err := os.ReadFile(ConfigFile)
|
|
|
+ if err != nil {
|
|
|
+ panic(fmt.Sprint("failed read config file: ", err))
|
|
|
+ }
|
|
|
+ err = yaml.Unmarshal(fcfg, &Cfg)
|
|
|
+ if err != nil {
|
|
|
+ panic(fmt.Sprint("failed parse yaml config: ", err))
|
|
|
+ }
|
|
|
+ if Cfg.SwapSize < 4 {
|
|
|
+ panic("swap size should not less than 4MB.")
|
|
|
+ }
|
|
|
+ if Cfg.BootFsSize < 4 {
|
|
|
+ panic("swap size should not less than 4MB.")
|
|
|
+ }
|
|
|
+ ubootLen, err := CheckSize("u-boot", Cfg.UBootImg)
|
|
|
+ if err != nil {
|
|
|
+ panic(err)
|
|
|
+ }
|
|
|
+ ubootMaxSize := int64(FIRST_PART_OFFSET - UBOOT_OFFSET)
|
|
|
+ if ubootLen > ubootMaxSize {
|
|
|
+ panic(fmt.Sprintf(
|
|
|
+ "The maximum size of u-boot is %d bytes, but yourimage has %d bytes, which exceeds %d bytes.",
|
|
|
+ ubootMaxSize, ubootLen, ubootMaxSize-ubootLen,
|
|
|
+ ))
|
|
|
+ }
|
|
|
+ CheckDir(Cfg.BootDir)
|
|
|
+ rootLen, err := CheckSize("root filesystem", Cfg.RootImg)
|
|
|
+ if err != nil {
|
|
|
+ panic(err)
|
|
|
+ }
|
|
|
+ upperLen, err := CheckSize("upper filesystem", Cfg.UpperImg)
|
|
|
+ if err != nil {
|
|
|
+ panic(err)
|
|
|
+ }
|
|
|
+ bootActualSize := Cfg.BootFsSize * 1024 * 1024
|
|
|
+ swapActualSize := Cfg.SwapSize * 1024 * 1024
|
|
|
+ p1start := AlignToBlockSize(FIRST_PART_OFFSET)
|
|
|
+ p1size := AlignToBlockSize(bootActualSize)
|
|
|
+ p2start := p1start + p1size
|
|
|
+ p2size := AlignToBlockSize(rootLen)
|
|
|
+ p3start := p2start + p2size
|
|
|
+ p3size := AlignToBlockSize(swapActualSize)
|
|
|
+ p4start := p3start + p3size
|
|
|
+ p4size := AlignToBlockSize(upperLen)
|
|
|
+ diskSize := int64(p4start+p4size)*BLOCK_SIZE + LAST_RESERVED
|
|
|
+ fmt.Println("==== Partition Table Brief ====")
|
|
|
+ PrintPartitionTableBrief([]PTBrief{
|
|
|
+ {
|
|
|
+ StartSector: int(p1start),
|
|
|
+ SizeSector: int(p1size),
|
|
|
+ ShowName: "boot",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ StartSector: int(p2start),
|
|
|
+ SizeSector: int(p2size),
|
|
|
+ ShowName: "root",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ StartSector: int(p3start),
|
|
|
+ SizeSector: int(p3size),
|
|
|
+ ShowName: "swap",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ StartSector: int(p4start),
|
|
|
+ SizeSector: int(p4size),
|
|
|
+ ShowName: "data",
|
|
|
+ },
|
|
|
+ })
|
|
|
+ fmt.Println("Total Size: ", humanize.IBytes(uint64(diskSize)))
|
|
|
+ fmt.Println("================================")
|
|
|
+ if !QuietMode {
|
|
|
+ if !ack.Acknowledge("Continue?", true) {
|
|
|
+ fmt.Println("User canceled.")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if !CheckExists() {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ idisk, err := diskfs.Create(OutputImg, diskSize, diskfs.Raw, diskfs.SectorSizeDefault)
|
|
|
+ table := &mbr.Table{
|
|
|
+ Partitions: []*mbr.Partition{
|
|
|
+ {
|
|
|
+ Bootable: false,
|
|
|
+ Type: mbr.Fat32LBA,
|
|
|
+ Start: p1start,
|
|
|
+ Size: p1size,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ Bootable: false,
|
|
|
+ Type: mbr.Linux,
|
|
|
+ Start: p2start,
|
|
|
+ Size: p2size,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ Bootable: false,
|
|
|
+ Type: mbr.LinuxSwap,
|
|
|
+ Start: p3start,
|
|
|
+ Size: p3size,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ Bootable: false,
|
|
|
+ Type: mbr.Linux,
|
|
|
+ Start: p4start,
|
|
|
+ Size: p4size,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ LogicalSectorSize: BLOCK_SIZE,
|
|
|
+ PhysicalSectorSize: BLOCK_SIZE,
|
|
|
+ }
|
|
|
+ err = idisk.Partition(table)
|
|
|
+ if err != nil {
|
|
|
+ panic(fmt.Sprint("failed create disk partition: ", err))
|
|
|
+ }
|
|
|
+ fmt.Println("processing Partition 1: boot...")
|
|
|
+ CreateFat32Fs(idisk, 1, Cfg.BootDir)
|
|
|
+ fmt.Println("processing Partition 2: root...")
|
|
|
+ CopyImgToPartition(idisk, 2, Cfg.RootImg)
|
|
|
+ fmt.Println("processing Partition 3: swap...")
|
|
|
+ FillPartition(idisk, 3, int(swapActualSize))
|
|
|
+ fmt.Println("processing Partition 4: data...")
|
|
|
+ CopyImgToPartition(idisk, 4, Cfg.UpperImg)
|
|
|
+ fmt.Println("writing bootloader...")
|
|
|
+ WriteUboot()
|
|
|
+ fmt.Println("==== Done ====")
|
|
|
+}
|
|
|
+
|
|
|
+func CheckExists() bool {
|
|
|
+ stat, err := os.Stat(OutputImg)
|
|
|
+ if err != nil {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ if !QuietMode {
|
|
|
+ if !ack.Acknowledge(fmt.Sprintf("'%s' already exists, override?", OutputImg), true) {
|
|
|
+ fmt.Println("User canceled.")
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if stat.IsDir() {
|
|
|
+ panic(fmt.Sprintf("'%s' is a existed dir", OutputImg))
|
|
|
+ }
|
|
|
+ err = os.Remove(OutputImg)
|
|
|
+ if err != nil {
|
|
|
+ panic(fmt.Sprintf("'%s' is a existed file, but can not remove: %v", OutputImg, err))
|
|
|
+ }
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+func AlignToBlockSize(size int64) uint32 {
|
|
|
+ remain := size % BLOCK_SIZE
|
|
|
+ m := size - remain
|
|
|
+ mr := m / BLOCK_SIZE
|
|
|
+ if remain > 0 {
|
|
|
+ mr = mr + 1
|
|
|
+ }
|
|
|
+ return uint32(mr)
|
|
|
+}
|
|
|
+
|
|
|
+func CheckSize(dname, fname string) (int64, error) {
|
|
|
+ fstat, err := os.Stat(fname)
|
|
|
+ if err != nil {
|
|
|
+ return 0, fmt.Errorf("can not stat %s image '%s': %v", dname, fname, err)
|
|
|
+ }
|
|
|
+ l := fstat.Size()
|
|
|
+ if l <= 0 {
|
|
|
+ return 0, fmt.Errorf("%s image is empty", dname)
|
|
|
+ }
|
|
|
+ return l, nil
|
|
|
+}
|
|
|
+
|
|
|
+func CheckDir(dname string) {
|
|
|
+ fstat, err := os.Stat(dname)
|
|
|
+ if err != nil {
|
|
|
+ panic(fmt.Sprintf("can not stat dir '%s': %v", dname, err))
|
|
|
+ }
|
|
|
+ if !fstat.IsDir() {
|
|
|
+ panic(fmt.Sprintf("'%s' is not a dir", dname))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+type PTBrief struct {
|
|
|
+ ShowName string
|
|
|
+ StartSector int
|
|
|
+ SizeSector int
|
|
|
+}
|
|
|
+
|
|
|
+func PrintPartitionTableBrief(brief []PTBrief) {
|
|
|
+ tb, err := gotable.Create("#", "Used For", "Offset(Sectors)", "Offset(Bytes)", "Size(Sectors)", "Size(Bytes)")
|
|
|
+ if err != nil {
|
|
|
+ panic(err)
|
|
|
+ }
|
|
|
+ for i, v := range brief {
|
|
|
+ err = tb.AddRow(map[string]string{
|
|
|
+ "#": strconv.Itoa(i + 1),
|
|
|
+ "Used For": v.ShowName,
|
|
|
+ "Offset(Sectors)": strconv.Itoa(v.StartSector),
|
|
|
+ "Offset(Bytes)": humanize.IBytes(uint64(v.StartSector) * BLOCK_SIZE),
|
|
|
+ "Size(Sectors)": strconv.Itoa(v.SizeSector),
|
|
|
+ "Size(Bytes)": humanize.IBytes(uint64(v.SizeSector) * BLOCK_SIZE),
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ panic(err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ fmt.Println(tb.String())
|
|
|
+}
|
|
|
+
|
|
|
+func CreateFat32Fs(d *disk.Disk, ptn int, from string) {
|
|
|
+ spec := disk.FilesystemSpec{Partition: ptn, FSType: filesystem.TypeFat32, VolumeLabel: "boot"}
|
|
|
+ ifs, err := d.CreateFilesystem(spec)
|
|
|
+ if err != nil {
|
|
|
+ panic(fmt.Sprintf("can not create filesystem for partition %d: %v", ptn, err))
|
|
|
+ }
|
|
|
+ err = filepath.Walk(from, func(path string, info fs.FileInfo, err error) error {
|
|
|
+ if err != nil {
|
|
|
+ fmt.Println("[Warn] error in '", path, "': ", err)
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ relPath, err := filepath.Rel(from, path)
|
|
|
+ if err != nil {
|
|
|
+ fmt.Println("[Warn] error in '", path, "': can not get rel path: ", err)
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ relPath = "/" + strings.TrimPrefix(relPath, ".")
|
|
|
+ if info.IsDir() {
|
|
|
+ err = ifs.Mkdir(relPath)
|
|
|
+ if err != nil {
|
|
|
+ fmt.Printf("[Warn] error in 'PART%d://%s': can not mkdir: %v\n", ptn, relPath, err)
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+ } else {
|
|
|
+ rw, xerr := ifs.OpenFile(relPath, os.O_CREATE|os.O_RDWR)
|
|
|
+ if xerr != nil {
|
|
|
+ fmt.Printf("[Warn] error in 'PART%d://%s': can not open file: %v\n", ptn, relPath, xerr)
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ defer rw.Close()
|
|
|
+ in, xerr := os.Open(path)
|
|
|
+ if xerr != nil {
|
|
|
+ fmt.Println("[Warn] error in '", path, "': can not open file: ", xerr)
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ defer in.Close()
|
|
|
+ _, xerr = io.Copy(rw, in)
|
|
|
+ if xerr != nil {
|
|
|
+ fmt.Printf("[Warn] io error in copy '%s' to 'PART%d://%s': %v\n", path, ptn, relPath, xerr)
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ panic(fmt.Sprint("error in copy files to partition 1: ", err))
|
|
|
+ }
|
|
|
+ _, ok := ifs.(*fat32.FileSystem)
|
|
|
+ if !ok {
|
|
|
+ panic(fmt.Errorf("create filesystem for partition %d failed: finally it is not a fat32 filesystem"))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func CopyImgToPartition(d *disk.Disk, ptn int, imgfile string) {
|
|
|
+ in, err := os.Open(imgfile)
|
|
|
+ if err != nil {
|
|
|
+ panic(fmt.Sprint("error in open img file '", imgfile, "': ", err))
|
|
|
+ }
|
|
|
+ defer in.Close()
|
|
|
+ n, err := d.WritePartitionContents(ptn, in)
|
|
|
+ if err != nil {
|
|
|
+ panic(fmt.Sprintf("error in writing img '%s' to partition %d: %v '", imgfile, ptn, err))
|
|
|
+ }
|
|
|
+ fmt.Printf("write %s into partition %d.\n", humanize.IBytes(uint64(n)), ptn)
|
|
|
+}
|
|
|
+
|
|
|
+func FillPartition(d *disk.Disk, ptn int, size int) {
|
|
|
+ in := NewSizableZeroReader(size)
|
|
|
+ n, err := d.WritePartitionContents(ptn, in)
|
|
|
+ if err != nil {
|
|
|
+ panic(fmt.Sprintf("error in filling partition %d: %v '", ptn, err))
|
|
|
+ }
|
|
|
+ fmt.Printf("fill %s into partition %d.\n", humanize.IBytes(uint64(n)), ptn)
|
|
|
+}
|
|
|
+
|
|
|
+func WriteUboot() {
|
|
|
+ fUboot, err := os.OpenFile(Cfg.UBootImg, os.O_RDONLY|os.O_EXCL, 0666)
|
|
|
+ if err != nil {
|
|
|
+ panic(fmt.Sprintf("error in open u-boot img: %v '", err))
|
|
|
+ }
|
|
|
+ defer fUboot.Close()
|
|
|
+ fDiskImg, err := os.OpenFile(OutputImg, os.O_EXCL|os.O_RDWR, 0666)
|
|
|
+ if err != nil {
|
|
|
+ panic(fmt.Sprintf("error in open disk img for writting bootloader: %v '", err))
|
|
|
+ }
|
|
|
+ defer fUboot.Close()
|
|
|
+ n, err := fDiskImg.Seek(UBOOT_OFFSET, 0)
|
|
|
+ if err != nil {
|
|
|
+ panic(fmt.Sprintf("error in seeking img for writting bootloader: %v '", err))
|
|
|
+ }
|
|
|
+ if n != UBOOT_OFFSET {
|
|
|
+ panic(fmt.Sprintf("error in seeking img for writting bootloader: current pos %d != UBOOT_OFFSET(%d) '", n, UBOOT_OFFSET))
|
|
|
+ }
|
|
|
+ n, err = io.Copy(fDiskImg, fUboot)
|
|
|
+ if err != nil {
|
|
|
+ panic(fmt.Sprintf("io error in writting bootloader: %v '", err))
|
|
|
+ }
|
|
|
+ fmt.Printf("%d bytes written.\n", n)
|
|
|
+}
|