require 'to_lvm_xfs' class Raspbian < Base Sizes = { '' => 1, 'b' => 1, 's' => 512, 'k' => 1024, 'm' => 1024*1024, 'g' => 1024*1024*1024, 't' => 1024*1024*1024*1024, 'p' => 1024*1024*1024*1024*1024, 'e' => 1024*1024*1024*1024*1024*1024, } Volume = Struct.new :sh, :name, :device, :mountpoint, :builder_mp, :size do def uuid() sh.fs_uuid device end def type() sh.fs_type device end def to_fstab_entry FSTabMountEntry.new "UUID=#{uuid}", mountpoint, type, 'defaults,noatime', nil, nil end end def initialize *args @vgname = "raspi_#{SecureRandom.urlsafe_base64 5}" @volumes = { root: ['/', '4.2G'], home: ['/home', '100M'] } super *args @vgpath = XPathname.new( '/dev') + @vgname STDERR.printf "Volumes:\n" vols = {} @volumes.each do |name, (mp, size)| mp = XPathname.new( mp).cleanpath vols[mp.to_s] = Volume.new sh, name.to_s, @vgpath+name.to_s, mp, dest.root + mp.to_s[1..-1], size STDERR.printf "%13s: %s (%s)\n", name, mp, size end @volumes = vols end def getopts opts super opts opts.on '-vNAME', '--volume=NAME', 'Creates additional volume name:mountpoint=size or overwrites defaults.' do |v| m = %r<\A([0-9a-z_-]+):(/[0-9a-z/_-]*)=([0-9.]+[%a-z]?)\z>i.match v fail "Volume expected in format: \"name:mountpoint=size\"" unless m @volumes[m[1].to_sym] = [XPathname.new( m[2]).cleanpath, m[3]] end end def build ENV['LANG'] = 'C' mkmppaths case when !dest.image.exist? when dest.image.file? r = sh.losetup_list unless r.empty? or r['loopdevices'] r['loopdevices'].each do |lo| fail "File #{dest.image} used as loop-device back-file" if +dest.image == +XPathname.new( lo['back-file']) end end when dest.image.blockdev?, dest.image.chardev? lsblk( dest.image).each do |l| fail "Device #{l[:name]} mounted at #{l[:mountpoint]}" if l[:mountpoint] end end task "Mount base image #{base.image}" do sh.partx -:u, base.image if base.image.blockdev? fail "Base image does not exist" unless base.image.exist? base_parts = kpartx base.image fail "two partitions in base expected, got: #{base_parts.inspect}" unless 2 == base_parts.length mount base_parts[1], base.root, -:oro mount base_parts[0], base.root+'boot', -:oro end dest.image.open 'w' do |f| size = @volumes.inject 136*1024*1024 do |s, (_n, vol)| m = /\A([0-9.]+)([bBsSkKmMgGtTpPeE]?)\z/i.match vol.size fail "invalid size: #{vol.size}" unless m s + m[1].to_f * Sizes[m[2].downcase] end f << 0.chr*4096 f.pos = size - 1 f.putc 0.chr end task "Partitioning destination image #{dest.image}" do sh.parted dest.image, *%w[-- mklabel msdos mkpart primary fat32 4MB 132MB mkpart primary ext2 132MB -1s set 2 LVM on print] sh.partx -:u, dest.image if dest.image.blockdev? end *dest_parts = begin lsblk( dest.image).select do |l| sh.dmsetup :remove, File.basename(l[:name]) if 'lvm' == l[:type] l[:name].start_with?( dest.image.to_s) and 'part' == l[:type] end.map {|l| XPathname.new l[:name] }.sort rescue Sh::ProcessError kpartx dest.image end fail "two partitions in destination expected" unless 2 == dest_parts.length task "Prepare boot-partition #{dest_parts[0]} and lvm #{dest_parts[1]}" do dest_parts[0].open( 'w') {|f| f << 0.chr*4*1024*1024 } dest_parts[1].open( 'w') {|f| f << 0.chr*4*1024*1024 } sh.vgscan '--cache' sh.pvcreate -:ff, dest_parts[1] sh.vgcreate vgname, dest_parts[1] @volumes.each {|_name, vol| sh.lvcreate "-n#{vol.name}", "-L#{vol.size}", vgname } sh.vgchange -:ae, vgname sh.mkvfat -:nboot, dest_parts[0] @volumes.each {|_name, vol| sh.mkxfs "-mreflink=1", "-L#{vol.name}", vol.device } end addmp = {run_udev: dest.root+'run/udev'} task "Mount all filesystems" do mount @vgpath+'root', dest.root (%i[boot]).each do |n| d = addmp[n] = dest.root+n.to_s d.mkdir unless d.exist? end mount dest_parts[0], addmp[:boot] @volumes.each do |_name, vol| path = vol.builder_mp next if dest.root == path || addmp.values.include?( path) path.mkdir unless path.exist? mount @vgpath+vol.name, path end end task "Copy raspbian from base to dest" do sh.rsync_all "#{base.root}/", dest.root end task "Mount all special filesystems" do (%i[dev proc sys]).each do |n| d = addmp[n] = dest.root+n.to_s d.mkdir unless d.exist? end mount '/dev', addmp[:dev], --:bind mount 'proc', addmp[:proc], -:tproc mount 'sysfs', addmp[:sys], -:tsysfs end task "Prepare users" do install_authorized_keys rename_user end task "Prepare /etc and /boot" do update_fstab @volumes.map { |_name, vol| vol.to_fstab_entry } + [ FSTabMountEntry.new( "UUID=#{sh.fs_uuid( dest_parts[0])}", '/boot', 'vfat', 'defaults', 1, 1)] addmp[:boot].join( 'config.txt').replace_i do |f| replace = { 'pi4' => { initramfs: 'initramfs initrd8.img followkernel', arm_64bit: 'arm_64bit=1' }, 'pi3' => { initramfs: 'initramfs initrd8.img followkernel', arm_64bit: 'arm_64bit=1' }, 'pi2' => { initramfs: 'initramfs initrd7.img followkernel', }, 'pi1' => { initramfs: 'initramfs initrd.img followkernel', }, 'pi0' => { initramfs: 'initramfs initrd.img followkernel', }, } blocks = [nil] content = Hash.new {|h,block| h[block] = [] } block = nil f.each_line do |l| l.chomp! case l when /\A\[([^\]]*)\]\z/ block = $1 blocks.push block when /\Ainitramfs / l = replace[block].delete :initramfs end content[block].push l end replace.each {|block, rpl| content[block] += rpl.values + [''] unless rpl.empty? } blocks.flat_map {|block| content[block] } end addmp[:boot].join( 'ssh').write '' addmp[:boot].join( 'cmdline.txt').replace_i do |f| lines = f.readlines fail "Only one line in cmdline.txt expected" unless 1 == lines.length opts = {} lines[0].split( ' ').each do |line| /^([^=]*)(?:=(.*))?/ =~ line opts[$1.to_sym] = $2 end opts[:root] = @vgpath+'root' opts[:rootfstype] = :xfs opts.delete :init opts.map {|k,v| v ? "#{k}=#{v}" : "#{k}" }.join(' ') end dest.root.join( 'etc').chdir do XPathname.glob( 'rc*.d/*resize2fs_once').each do |fn| fn.unlink end end set_hostname end preload, preload_x = dest.root+'etc/ld.so.preload', dest.root+'etc/ld.so.preload.tp' task "Prepare to chroot to raspbian" do @qemu_bin.copy @qemu_bin_src, preserve: true after { @qemu_bin.unlink } if preload.exist? preload.rename preload_x after { preload_x.rename preload } end end task "update, upgrade and install" do ish = sh.chroot( dest.root).chdir( '/') ish.apt :update ish.apt :upgrade, -:y ish.apt :update install_packages_from_dir( XPathname.new( $0).expand_path.dirname + 'raspbian-files', XPathname.new( 'files') ) # We mount /run/udev for lvm-scanning - vgs / vgcfgbackup need it to connect to udev. addmp[:run_udev].mkdir mount '/run/udev', addmp[:run_udev], --:bind # prevent installing exim by installing nullmailer #ish.apt *%w[install -y lvm2 xfsprogs nullmailer dracut] #dest.root.join( 'etc/dracut.conf.d/10-denkn.conf').open 'w' do |f| # f.puts 'add_modules+="lvm"' # f.puts 'add_drivers+="dm-mod xfs"' # f.puts 'compress="xz"' #end ish.apt *%w[install -y lvm2 xfsprogs initramfs-tools] dest.root.join( 'etc/initramfs-tools/initramfs.conf').replace_i do |f| replace = { compress: 'COMPRESS=xz', } f.each_line.flat_map do |l| case l.chomp! when /^COMPRESS=/ then replace.delete :compress when /^# *COMPRESS=/ then [l, replace.delete( :compress)] else l end end + replace.values end # strange error while generating initramfs; # it want to delete this directory, but it does not exist. #overlays = dest.root + 'usr/share/rpikernelhack/overlays' #overlays.mkpath #overlays.join( '.keep').write '' # generates implicite initramfs ish.system *%w[dpkg-reconfigure raspberrypi-kernel] end end end