commit
3eb91f4708
@ -0,0 +1,155 @@ |
||||
#!/usr/bin/env ruby |
||||
|
||||
require 'pathname' |
||||
$: << Pathname.new($0).expand_path.dirname+'lib' |
||||
require 'base' |
||||
require 'active_support/all' |
||||
|
||||
Sector = Sectors = 512 |
||||
|
||||
class Armbian2LVM_XFS < Base |
||||
def initialize vgname = nil |
||||
super |
||||
chs = [?a..?z, ?A..?Z].map(&:to_a).flatten |
||||
@vgname = vgname || "armbian_#{5.times.map{chs[rand(chs.length)]}.join}" |
||||
end |
||||
|
||||
def remove_all_partitions_from image |
||||
o = `parted -ms #{image.to_s.shellescape} print` |
||||
d "Cannot determine partitions in dest-image #{image}", 0 == $? |
||||
o.split( "\n")[2..-1].each do |
||||
sh.parted image, :rm, 1 |
||||
end |
||||
end |
||||
|
||||
def run |
||||
mkmppaths |
||||
|
||||
d "Base image does not exist", base.image.exist? |
||||
base_parts = kpartx base.image |
||||
d "two (root-)partition in base expected, got: #{base_parts.inspect}", 1 == base_parts.length |
||||
|
||||
mount -:oro, base_parts[0], base.root |
||||
|
||||
# creates an image |
||||
dest.image.open 'w' do |f| |
||||
#f.pos = 1*Sector |
||||
## copy first 4mb from base-image because of u-boot |
||||
#f.write base.image.read( 4.megabytes-Sector, Sector) |
||||
f.pos = 1.6.gigabytes-1 |
||||
f.putc 0.chr |
||||
end |
||||
# partitions for: /boot (128MiBi), LVM |
||||
sh.parted dest.image, *%w[-- |
||||
mklabel msdos |
||||
mkpart primary ext2 4MB 132MB |
||||
mkpart primary ext2 132MB -1s |
||||
set 2 LVM on |
||||
print] |
||||
|
||||
sh.bash -:ec, <<-EOF.gsub( /^\t{3}/, '').sub( /\n$/m, '') |
||||
BRANCH=dev |
||||
source ./build/config/sources/odroidxu4.conf |
||||
write_uboot_platform base/root/usr/lib/linux-u-boot-dev-odroidxu4_5.34.171103_armhf dest/base |
||||
EOF |
||||
|
||||
dest_parts = kpartx dest.image |
||||
d "two partitions in destination expected", 2 == dest_parts.length |
||||
vgpath = Pathname.new( '/dev') + vgname |
||||
sh.pvcreate dest_parts[1] |
||||
sh.vgcreate vgname, dest_parts[1] |
||||
sh.lvcreate -:nroot, '-L1.1G', vgname |
||||
sh.lvcreate -:nhome, '-L100M', vgname |
||||
sh.vgchange -:ae, vgname |
||||
sh.mkext4fs dest_parts[0] |
||||
sh.mkxfs vgpath+'root' |
||||
sh.mkxfs vgpath+'home' |
||||
|
||||
mount vgpath+'root', dest.root |
||||
addmp = {} |
||||
%i[home boot].each do |n| |
||||
d = addmp[n] = dest.root+n.to_s |
||||
d.mkdir |
||||
end |
||||
mount vgpath+'home', addmp[:home] |
||||
mount dest_parts[0], addmp[:boot] |
||||
|
||||
sh.rsync_all "#{base.root}/", dest.root |
||||
|
||||
(addmp[:boot]+'boot').make_symlink '.' # boot -> . |
||||
(addmp[:boot]+'boot.ini').open 'r+' do |f| |
||||
replace = { |
||||
rootdev: "setenv rootdev \"#{vgpath+'root'}\"", |
||||
rootfstype: "setenv rootfstype \"xfs\"", |
||||
} |
||||
lines = |
||||
f.each_line.map do |l| |
||||
l.chomp! |
||||
case l |
||||
when /^setenv rootdev / |
||||
replace.delete :rootdev |
||||
when /^setenv rootfstype / |
||||
replace.delete :rootfstype |
||||
else |
||||
l.gsub ' /boot/', ' /' |
||||
end |
||||
end |
||||
f.truncate 0 |
||||
f.pos = 0 |
||||
f.puts *lines |
||||
d "boot.ini had no entry for: #{replace.keys}", replace.empty? |
||||
end |
||||
|
||||
(dest.root+'etc'+'fstab').open 'r+' do |f| |
||||
replace = { |
||||
'/' => "#{vgpath+'root'} / xfs defaults,noatime 0 0", |
||||
'/boot' => "#{dest_parts[0]} /boot ext4 defaults,noatime 0 0", |
||||
'/home' => "#{vgpath+'home'} /home xfs defaults,noatime 0 0", |
||||
} |
||||
lines = |
||||
f.each_line.flat_map do |l| |
||||
mp = l.split( /\s+/)[1] |
||||
replace.delete( mp) || l |
||||
end |
||||
lines += replace.values |
||||
f.truncate 0 |
||||
f.pos = 0 |
||||
f.puts lines |
||||
end |
||||
|
||||
(dest.root+'etc'+'initramfs-tools'+'initramfs.conf').open 'r+' do |f| |
||||
lines = |
||||
f.each_line.flat_map do |l| |
||||
l.chomp! |
||||
case l |
||||
when /^COMPRESS=/ |
||||
'COMPRESS=xz' |
||||
when /^# *COMPRESS=/ |
||||
[l, 'COMPRESS=xz'] |
||||
else |
||||
l |
||||
end |
||||
end |
||||
f.truncate 0 |
||||
f.pos = 0 |
||||
f.puts lines |
||||
end |
||||
|
||||
sh.tar -:C, dest.root, -:xf, 'files.tar' if File.exists? 'files.tar' |
||||
|
||||
rescue ProgrammError |
||||
err $! |
||||
raise |
||||
ensure |
||||
STDERR.puts "="*80 |
||||
umount_all ignore_exceptions: true |
||||
sh.vgchange -:an, vgname rescue Object |
||||
sh.sync rescue Object |
||||
departx dest.image rescue Object |
||||
departx base.image rescue Object |
||||
sh.sync rescue Object |
||||
STDERR.puts "="*80 |
||||
end |
||||
end |
||||
|
||||
Armbian2LVM_XFS.new(*ARGV).run |
@ -0,0 +1 @@ |
||||
require 'to_lvm_xfs/base' |
@ -0,0 +1,380 @@ |
||||
require 'pathname' |
||||
require 'shellwords' |
||||
require 'optparse' |
||||
require 'securerandom' |
||||
require 'to_lvm_xfs/exts' |
||||
require 'to_lvm_xfs/sh' |
||||
require 'to_lvm_xfs/structs' |
||||
|
||||
class ProgrammError < RuntimeError |
||||
end |
||||
|
||||
class Base |
||||
def self.run *args, &exe |
||||
new( *args).instance_eval &exe |
||||
end |
||||
|
||||
def d msg, eq |
||||
raise ProgrammError, msg, caller[1..-1] unless eq |
||||
end |
||||
|
||||
def err *args |
||||
STDERR.puts "\e[31;1m#{args.join ' '}\e[0m" |
||||
end |
||||
|
||||
def msg first, *args |
||||
STDERR.puts "\e[33m#{first}\e[35m #{args.join ' '}\e[0m" |
||||
end |
||||
|
||||
def kpartx image, read_only: nil |
||||
sh.kpartx "-sa#{read_only ? :r : ''}", image |
||||
lpartx image |
||||
end |
||||
|
||||
def lpartx image |
||||
mapper = Pathname.new '/dev/mapper' |
||||
lines = sh.kpartx( -:al, image, return: :lines)[0..-1] |
||||
if lines.grep( /^loop deleted/).empty? |
||||
lines.map do |line| |
||||
d "cannot extract partition «#{line}»", line =~ /^([^ :]+) : / |
||||
mapper + $1 |
||||
end |
||||
else |
||||
[] |
||||
end |
||||
end |
||||
|
||||
def lsblk path, *output |
||||
output = %w[NAME TYPE MOUNTPOINT] if output.empty? |
||||
output = output.map {|o| o.to_s } |
||||
lines = |
||||
sh.lsblk %w[--output], output.join(','), path |
||||
op = output.map {|o| o.downcase.to_sym } |
||||
lines.map {|l| Hash[*op.zip(l.split( /\s+/)).flatten]} |
||||
end |
||||
|
||||
def departx image |
||||
sh.kpartx -:sd, image |
||||
end |
||||
|
||||
def kpartxed? image |
||||
lines = sh.kpartx -:l, image, return: :lines |
||||
not lines.grep( /^loop deleted/).empty? |
||||
end |
||||
|
||||
|
||||
def get_vgname_of_pv *a, &exe |
||||
return to_enum( __method__, *a) unless block_given? |
||||
sh.pvs( --:options, :vg_name, *a, return: :lines).each do |l| |
||||
yield l.chomp.sub( /^\s*/, '') |
||||
end |
||||
end |
||||
|
||||
def remove_all_partitions_from image |
||||
sh[].parted( -:ms, image, :print).each do |
||||
sh.parted image, :rm, 1 |
||||
end |
||||
end |
||||
|
||||
def manipulate_file file, &exe |
||||
flags = 'r+' |
||||
msg :manipulate, file |
||||
File.open file.to_s, flags do |file| |
||||
lines = [] |
||||
exe.call file, &lines.method(:push) |
||||
file.truncate 0 |
||||
file.pos = 0 |
||||
lines.each &file.method(:puts) |
||||
end |
||||
end |
||||
|
||||
def capsulated_rescue |
||||
yield |
||||
rescue Object => e |
||||
STDERR.puts "\e[31;1m#{e} (#{e.class}):" |
||||
e.backtrace.each {|s| STDERR.puts " #{s}" } |
||||
STDERR.print "\e[0m" |
||||
end |
||||
|
||||
attr_reader :sh, :mounted, :looped, :base, :dest, :vgname |
||||
def initialize *args |
||||
OptionParser.new do |opts| |
||||
getopts opts |
||||
opts.parse! args |
||||
end |
||||
|
||||
if :ask == @password |
||||
v = b = nil |
||||
require 'io/console' |
||||
STDERR.print "Type Password. " |
||||
v = STDIN.getpass |
||||
STDERR.print "Type password again. " |
||||
b = STDIN.getpass |
||||
d "Password missmatch", v == b |
||||
d "Password is empty", !v.empty? |
||||
@password = v.crypt "$6$#{SecureRandom.urlsafe_base64 6}$" |
||||
end |
||||
|
||||
@sh = Sh.new immediately: true, expect_status: 0 |
||||
#x = sh.system :echo, :hallo, :welt, as_io: true do |io| |
||||
# STDERR.puts io.inspect |
||||
# STDERR.puts '---',io.readlines,'---' |
||||
#end |
||||
#STDERR.puts x.inspect |
||||
#exit 1 |
||||
@mounted, @activated_vgs = [], [] |
||||
@looped = Hash.new {|h,k| h[k] = [] } |
||||
@base = Image.new self, 'base', image: @baseimage |
||||
@dest = Image.new self, 'dest', image: @destination |
||||
|
||||
STDERR.print <<EOF |
||||
Settings: |
||||
username: #{@username || '(default)'} |
||||
password: #{@password ? '*********' : '(default)'} |
||||
baseimage: #{@base.image} |
||||
destination: #{@dest.image} |
||||
vgname: #{@vgname} |
||||
hostname: #{@hostname || '(default)'} |
||||
EOF |
||||
|
||||
sh.def_system_commands *%i[echo sed mount umount kpartx sync rsync xz gzip bzip2 zip tar bash dpkg apt] |
||||
sh.def_system_commands *%i[dmsetup lvcreate vgcreate pvcreate vgchange mkswap vgscan] |
||||
sh.alias_command :pvs, *%w[pvs --noheadings] |
||||
sh.alias_command :blkid, 'blkid', return: :line |
||||
sh.alias_command :lsblk, *%w[lsblk --noheadings --paths --list], return: :lines |
||||
sh.alias_command :parted, *%w[parted -ms] |
||||
sh.alias_command :mkxfs, *%w[mkfs.xfs -f] |
||||
sh.alias_command :mkext2fs, *%w[mkfs.ext2] |
||||
sh.alias_command :mkext4fs, *%w[mkfs.ext4] |
||||
sh.alias_command :mkvfat, *%w[mkfs.vfat] |
||||
sh.alias_command :rsync_all, *%w[rsync -aHAX] |
||||
sh.alias_command :mount_ro, *%w[mount -oro] |
||||
sh.alias_command :fs_uuid, *%w[blkid -o value -s UUID], return: :line |
||||
sh.alias_command :fs_type, *%w[blkid -o value -s TYPE], return: :line |
||||
end |
||||
|
||||
def getopts opts |
||||
opts.on '-h', '--help' do |
||||
STDERR.puts opts |
||||
exit 1 |
||||
end |
||||
|
||||
opts.on '-bIMAGE', '--baseimage=IMAGE', 'Write image to IMAGE. Device or file' do |v| |
||||
@baseimage = v |
||||
end |
||||
|
||||
opts.on '-dIMAGE', '--destination=IMAGE', 'Write image to IMAGE. Device or file' do |v| |
||||
@destination = v |
||||
end |
||||
|
||||
opts.on '-uUSER', '--user=USER', 'Change username of user pi to this USERname' do |v| |
||||
@username = v |
||||
end |
||||
|
||||
opts.on '-W', '--ask-pass', 'Ask for password for user via CLI.' do |
||||
@password = :ask |
||||
end |
||||
|
||||
opts.on '-wPASSWORD', '--password=PASSWORD', 'Set password for user' do |v| |
||||
@password = v.crypt "$6$#{SecureRandom.urlsafe_base64 6}$" |
||||
end |
||||
|
||||
opts.on '-nVGNAME', '--vgname=VGNAME', 'Name for volume group' do |v| |
||||
@vgname = v |
||||
end |
||||
|
||||
opts.on '-HNAME', '--hostname=NAME', 'Set hostname to NAME' do |v| |
||||
@hostname = v |
||||
end |
||||
|
||||
opts.on '-aAUTHKEYS', '--auth-keys=AUTHKEYS', 'SSH-Keys for user' do |f| |
||||
f = Pathname.new f |
||||
d "#{f} not found.", f.exist? |
||||
@authorized_keys = f |
||||
end |
||||
|
||||
opts.on '-AAUTHKEYS', '--root-auth-keys=AUTHKEYS', 'SSH-Keys for root' do |f| |
||||
f = Pathname.new f |
||||
d "#{f} not found.", f.exist? |
||||
@root_authorized_keys = f |
||||
end |
||||
end |
||||
|
||||
def run |
||||
build |
||||
|
||||
qemu_bin = dest.root + "usr/bin/qemu-arm-static" |
||||
if qemu_bin.exist? |
||||
msg :remove, "/usr/bin/qemu-arm-static" |
||||
qemu_bin.unlink |
||||
end |
||||
|
||||
rescue ProgrammError |
||||
err $! |
||||
raise |
||||
ensure |
||||
STDERR.puts "="*80 |
||||
umount_all ignore_exceptions: true |
||||
umount dest.root, -:R rescue Object |
||||
umount base.root, -:R rescue Object |
||||
sh.vgchange -:an, vgname rescue Object |
||||
sh.sync rescue Object |
||||
departx dest.image rescue Object |
||||
departx base.image rescue Object |
||||
sh.sync rescue Object |
||||
STDERR.puts "="*80 |
||||
end |
||||
|
||||
def activate_vg vgname |
||||
return nil if @activated_vgs.include? vgname |
||||
@activated_vgs.unshift vgname |
||||
sh.vgchange -:ae, vgname |
||||
end |
||||
|
||||
def deactivate_vg vgname |
||||
return nil unless @activated_vgs.include? vgname |
||||
sh.vgchange -:an, vgname |
||||
@activated_vgs.delete vgname |
||||
end |
||||
|
||||
def deactivate_all_vgs ignore_exceptions: nil |
||||
until @activated_vgs.empty? |
||||
if ignore_exceptions |
||||
capsulated_rescue { sh.vgchange -:an, @activated_vgs.pop } |
||||
else |
||||
sh.umount @activated_vgs.pop |
||||
end |
||||
end |
||||
end |
||||
|
||||
def mount from, to, *opts, &exe |
||||
@mounted.push to |
||||
sh.mount *opts, from, to |
||||
if block_given? |
||||
begin |
||||
yield |
||||
ensure |
||||
umount to |
||||
end |
||||
end |
||||
end |
||||
|
||||
def umount mp, *opts |
||||
r = sh.umount *opts, mp |
||||
@mounted.delete to |
||||
r |
||||
end |
||||
|
||||
def umount_all ignore_exceptions: nil |
||||
until @mounted.empty? |
||||
if ignore_exceptions |
||||
capsulated_rescue { sh.umount @mounted.pop } |
||||
else |
||||
sh.umount @mounted.pop |
||||
end |
||||
end |
||||
end |
||||
|
||||
def mkmppaths |
||||
[base, dest].each do |mp| |
||||
mp.dir.mkpath |
||||
mp.root.mkpath |
||||
end |
||||
end |
||||
|
||||
def ssh_copy_id_local src, home, uid, gid = nil |
||||
akf = home+'.ssh/authorized_keys' |
||||
akd = akf.dirname |
||||
akd.mkdir |
||||
akd.chown uid, gid |
||||
akd.chmod 0700 |
||||
akf.copy src |
||||
akf.chown uid, gid |
||||
akf.chmod 0600 |
||||
end |
||||
|
||||
def rename_user |
||||
ssh_copy_id_local @authorized_keys, dest.root+'home/pi', 1000, 1000 if @authorized_keys |
||||
ssh_copy_id_local @root_authorized_keys, dest.root+'root', 0, 0 if @root_authorized_keys |
||||
|
||||
if @username |
||||
dest.root.join( 'etc/passwd').replace_i do |f| |
||||
f.each_line.flat_map do |l| |
||||
user, pwd, uid, gid, name, home, shell = l.split( ':') |
||||
user, home = @username, "/home/#{@username}" if 'pi' == user |
||||
[user, pwd, uid, gid, name, home, shell].join ':' |
||||
end |
||||
end |
||||
|
||||
dest.root.join( 'etc/group').replace_i do |f| |
||||
f.each_line.flat_map do |l| |
||||
group, pwd, gid, users = l.split( ':') |
||||
group = @username if 'pi' == group |
||||
users = users.split( ',').map {|user| 'pi' == user ? @username : user } |
||||
[group, pwd, gid, users.join( ',')].join ':' |
||||
end |
||||
end |
||||
|
||||
dest.root.join( 'home/pi').rename dest.root.join( 'home', @username) |
||||
end |
||||
|
||||
if @password or @username |
||||
dest.root.join( 'etc/shadow').replace_i do |f| |
||||
f.each_line.flat_map do |l| |
||||
user, crypt, lastchange, minage, maxage, warn, inact, expiry, reserved = l.split( ':') |
||||
if 'pi' == user |
||||
crypt = @password if @password |
||||
user = @username if @username |
||||
end |
||||
[user, crypt, lastchange, minage, maxage, warn, inact, expiry, reserved].join ':' |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
||||
def set_hostname hostname = nil |
||||
hostname = @hostname |
||||
return if hostname.nil? or hostname.empty? |
||||
msg :patch, 'etc/hosts', 'set hostname to', hostname |
||||
dest.root.join( 'etc/hosts').replace_i do |f| |
||||
f.each_line.map {|l| l.gsub /\<raspberrypi\>/, hostname } |
||||
end |
||||
msg :write, hostname, :to, 'etc/hostname' |
||||
dest.root.join( 'etc/hostname').open 'w' do |f| |
||||
f.puts hostname |
||||
end |
||||
end |
||||
|
||||
def install_packages_from_dir *paths |
||||
paths.each do |pkgs| |
||||
next unless pkgs.directory? |
||||
pkgs.each_child do |pn| |
||||
next if /\A\./ =~ pn.basename.to_s # no dot-files. |
||||
install_package pn |
||||
end |
||||
end |
||||
end |
||||
|
||||
def install_package path |
||||
case path.basename.to_s |
||||
when /\.deb\z/ |
||||
install_deb path |
||||
when /\.tar\z/, /\.t(?:ar\.|)(?:xz|bzip2|gz|lzma|Z)\z/ |
||||
sh.tar -:C, dest.root, -:xf, path |
||||
else |
||||
warn :ignore, path |
||||
end |
||||
end |
||||
|
||||
def install_deb path |
||||
#sh.dpkg '--unpack', '--force-architecture', pn, chroot: dest.root |
||||
adeb = Pathname.new( 'var/cache/apt/archives')+path.basename.to_s |
||||
dest.root.join( adeb.to_s).copy path |
||||
sh.dpkg -:i, adeb, chroot: dest.root |
||||
#msg :unpack, path |
||||
# sometimes, you have to use system to do something. but it is ok, if you know, how to do it. |
||||
# this command should be safe. |
||||
#d "unpacking failed: #{path}", |
||||
# system( "ar p #{path.to_s.shellescape} data.tar.xz | tar -JxC #{dest.root.to_s.shellescape}") |
||||
end |
||||
end |
@ -0,0 +1,38 @@ |
||||
require 'pathname' |
||||
|
||||
class Pathname |
||||
alias +@ to_s |
||||
|
||||
def chdir &exe |
||||
Dir.chdir self.to_s, &exe |
||||
end |
||||
alias cd chdir |
||||
|
||||
# copies content of src in self as destination |
||||
def copy src, **opts |
||||
FileUtils.copy_file src.to_s, self.to_s, **opts |
||||
end |
||||
|
||||
def replace_i |
||||
open 'r+' do |f| |
||||
lines = yield f |
||||
f.truncate 0 |
||||
f.pos = 0 |
||||
f.puts lines |
||||
end |
||||
end |
||||
end |
||||
|
||||
class Symbol |
||||
alias +@ to_s |
||||
def -@ |
||||
:"-#{self}" |
||||
end |
||||
end |
||||
|
||||
class File |
||||
def reset |
||||
f.truncate 0 |
||||
f.pos = 0 |
||||
end |
||||
end |
@ -0,0 +1,160 @@ |
||||
require 'to_lvm_xfs' |
||||
|
||||
class Raspibian < Base |
||||
def initialize *args |
||||
@vgname = "raspi_#{SecureRandom.urlsafe_base64 5}" |
||||
super *args |
||||
end |
||||
|
||||
def build |
||||
ENV['LANG'] = 'C' |
||||
mkmppaths |
||||
|
||||
lsblk( dest.image).each do |l| |
||||
d "Device #{l[:name]} mounted at #{l[:mountpoint]}", ! l[:mountpoint] |
||||
end |
||||
|
||||
d "Base image does not exist", base.image.exist? |
||||
base_parts = kpartx base.image |
||||
d "two partitions in base expected, got: #{base_parts.inspect}", 2 == base_parts.length |
||||
mount base_parts[1], base.root, -:oro |
||||
mount base_parts[0], base.root+'boot', -:oro |
||||
|
||||
dest.image.open 'w' do |f| |
||||
f << 0.chr*4096 |
||||
f.pos = 4.8*1024*1024*1024-1 |
||||
f.putc 0.chr |
||||
end |
||||
sh.parted dest.image, *%w[-- |
||||
mklabel msdos |
||||
mkpart primary fat32 4MB 132MB |
||||
mkpart primary ext2 132MB -1s |
||||
set 2 LVM on |
||||
print] |
||||
|
||||
*dest_parts = |
||||
begin |
||||
lsblk( dest.image).select do |l| |
||||
STDERR.puts 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| Pathname.new l[:name] }.sort |
||||
rescue Sh::ProcessError |
||||
kpartx dest.image |
||||
end |
||||
d "two partitions in destination expected", 2 == dest_parts.length |
||||
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' |
||||
vgpath = Pathname.new( '/dev') + vgname |
||||
sh.pvcreate '-ff', dest_parts[1] |
||||
sh.vgcreate vgname, dest_parts[1] |
||||
sh.lvcreate -:nroot, '-L4.2G', vgname |
||||
sh.lvcreate -:nhome, '-L100M', vgname |
||||
sh.vgchange -:ae, vgname |
||||
sh.mkvfat -:nboot, dest_parts[0] |
||||
sh.mkxfs -:Lroot, vgpath+'root' |
||||
sh.mkxfs -:Lhome, vgpath+'home' |
||||
mount vgpath+'root', dest.root |
||||
addmp = {} |
||||
%i[home boot].each do |n| |
||||
d = addmp[n] = dest.root+n.to_s |
||||
d.mkdir |
||||
end |
||||
mount vgpath+'home', addmp[:home] |
||||
mount dest_parts[0], addmp[:boot] |
||||
|
||||
sh.rsync_all "#{base.root}/", dest.root |
||||
#sh.rsync *%w[kernel7.img initrd7.img], addmp[:boot] |
||||
|
||||
rename_user |
||||
|
||||
msg :patch, 'etc/fstab' |
||||
(dest.root+'etc'+'fstab').replace_i do |f| |
||||
replace = { |
||||
'/' => "UUID=#{sh.fs_uuid vgpath+'root'} / xfs defaults,noatime 0 0", |
||||
'/boot' => "UUID=#{sh.fs_uuid dest_parts[0]} /boot vfat defaults 1 1", |
||||
'/home' => "UUID=#{sh.fs_uuid vgpath+'home'} /home xfs defaults,noatime 1 1", |
||||
} |
||||
f.each_line.flat_map do |l| |
||||
mp = l.split( /\s+/)[1] |
||||
replace.delete( mp) || l |
||||
end + replace.values |
||||
end |
||||
|
||||
msg :patch, 'boot/config.txt' |
||||
(addmp[:boot]+'config.txt').replace_i do |f| |
||||
replace = { |
||||
initramfs: 'initramfs initrd7.img followkernel', |
||||
} |
||||
f.each_line.flat_map do |l| |
||||
l.chomp! |
||||
case l |
||||
when /^initramfs / |
||||
replace.delete :initramfs |
||||
else l |
||||
end |
||||
end + replace.values |
||||
end |
||||
|
||||
msg :touch, 'boot/ssh' |
||||
(addmp[:boot]+'ssh').open('w') {|f|} |
||||
|
||||
msg :patch, 'boot/cmdline.txt' |
||||
(addmp[:boot]+'cmdline.txt').replace_i do |f| |
||||
lines = f.readlines |
||||
d "Only one line in cmdline.txt expected", 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 |
||||
|
||||
msg :patch, 'etc/initramfs-tools/initramfs.conf' |
||||
dest.root.join( 'etc/initramfs-tools/initramfs.conf').replace_i do |f| |
||||
f.each_line.flat_map do |l| |
||||
l.chomp! |
||||
case l |
||||
when /^COMPRESS=/ |
||||
'COMPRESS=xz' |
||||
when /^# *COMPRESS=/ |
||||
[l, 'COMPRESS=xz'] |
||||
else |
||||
l |
||||
end |
||||
end |
||||
end |
||||
|
||||
set_hostname |
||||
|
||||
msg :unlinking, 'etc/rc*.d/*resize2fs_once' |
||||
(dest.root+'etc').chdir do |
||||
Pathname.glob( 'rc*.d/*resize2fs_once').each do |fn| |
||||
fn.unlink |
||||
end |
||||
end |
||||
|
||||
qemu_bin = dest.root.join 'usr/bin/qemu-arm-static' |
||||
msg :copy, "/usr/bin/qemu-arm-static" |
||||
qemu_bin.copy '/usr/bin/qemu-arm-static', preserve: true |
||||
|
||||
ish = sh.chroot dest.root |
||||
ish.apt :update |
||||
ish.apt :upgrade, -:y |
||||
ish.apt :update |
||||
ish.apt :install, -:y, :lvm2, :xfsprogs |
||||
|
||||
install_packages_from_dir( |
||||
Pathname.new($0).expand_path.dirname+'raspbian-files', |
||||
Pathname.new('files') |
||||
) |
||||
|
||||
# generates implicite initramfs |
||||
ish.system *%w[dpkg-reconfigure raspberrypi-kernel] |
||||
end |
||||
end |
@ -0,0 +1,246 @@ |
||||
require 'forwardable' |
||||
require 'shellwords' |
||||
require 'socket' |
||||
|
||||
class Sh |
||||
class ForkError < RuntimeError |
||||
end |
||||
class ForkFailed < ForkError |
||||
end |
||||
class ProcessError < ForkError |
||||
end |
||||
|
||||
class Fork |
||||
extend Forwardable |
||||
def_delegators :@status, :exitstatus |
||||
attr_reader :pid, :status |
||||
|
||||
def initialize &exe |
||||
#@io, s = UNIXSocket.pair |
||||
@pid = |
||||
Process.fork do |
||||
Thread.list.each do |th| |
||||
th.kill unless Thread.current == th |
||||
end |
||||
#STDOUT.reopen s.dup |
||||
#STDIN.reopen s |
||||
#STDOUT.close_read |
||||
#STDIN.close_write |
||||
yield |
||||
end |
||||
raise ForkFailed unless @pid |
||||
#s.close |
||||
end |
||||
|
||||
def wait |
||||
_, @status = Process.waitpid2 @pid |
||||
@status |
||||
end |
||||
|
||||
def kill signal = nil |
||||
Process.kill signal||9, @pid |
||||
end |
||||
|
||||
def ok?() 0 == @status.exitstatus end |
||||
def failed?() 0 != @status.exitstatus end |
||||
end |
||||
|
||||
class IOFork < Fork |
||||
extend Forwardable |
||||
def_delegators :@io, :close, :closed?, :close_read, :closed_read?, :close_write, :closed_write? |
||||
def_delegators :@io, :getbyte, :getc, :getch, :getpass, :gets |
||||
def_delegators :@io, :read, :read_nonblock, :readbyte, :readchar, :readline, :readlines, :readpartial, :sysread |
||||
def_delegators :@io, :syswrite, :write, :write_nonblock |
||||
def_delegators :@io, :putc, :puts, :print, :printf |
||||
def_delegators :@io, :each_line |
||||
attr_reader :io |
||||
|
||||
def initialize &exe |
||||
@io, s = UNIXSocket.pair |
||||
r = nil |
||||
super do |
||||
@io.close |
||||
STDOUT.reopen s |
||||
STDIN.reopen s |
||||
#STDOUT.close_read |
||||
#STDIN.close_write |
||||
r = yield |
||||
end |
||||
s.close |
||||
r |
||||
end |
||||
end |
||||
|
||||
class Command |
||||
attr_reader :shell, :command, :args, :opts |
||||
|
||||
def initialize shell, command, *args, **opts |
||||
@shell, @command, @args, @opts = shell, command, args, opts |
||||
end |
||||
|
||||
def fork **opts |
||||
opts = @opts.merge opts |
||||
cl = |
||||
case opts[:mode] |
||||
when :io then IOFork |
||||
when Class then opts[:mode] |
||||
else Fork |
||||
end |
||||
cl.new do |
||||
Dir.chdir @shell.opts[:pwd] if @shell.opts[:pwd] |
||||
cmd = @command.to_s |
||||
$0 = cmd |
||||
#STDERR.printf "\n|\n| %p\n|\n", cmd: cmd, args: @args |
||||
if opts.has_key? :stdio |
||||
stdin, stdout, stderr = *opts[:stdio] |
||||
stdin ? STDIN.reopen( stdin) : STDIN.close |
||||
STDOUT.reopen stdout |
||||
STDERR.reopen stderr if stderr |
||||
end |
||||
if opts[:chroot] |
||||
STDERR.printf "\e[1;34mchroot(%s)\e[0m ", opts[:chroot].to_s |
||||
Dir.chroot opts[:chroot].to_s |
||||
end |
||||
#STDERR.puts "\e[0m#{opts[:return] ? '<=' : '#'} \e[33m#{cmd.shellescape} \e[35m#{usable_args.shelljoin}\e[0m" |
||||
STDERR.printf "\e[0m%s \e[33m%s \e[35m%s\e[0m\n", |
||||
opts[:return] ? '<=' : '#', |
||||
cmd.shellescape, |
||||
usable_args.shelljoin |
||||
exec cmd, *usable_args |
||||
raise ForkFailed, "Cannot execute" |
||||
end |
||||
rescue ForkFailed => e |
||||
raise ForkFailed, "<<#{self}>> cannot be forked" |
||||
end |
||||
|
||||
def usable_args |
||||
@uargs ||= @args.flatten.map &:to_s |
||||
end |
||||
|
||||
def to_s |
||||
"#{@command.to_s.shellescape}" + (usable_args.empty? ? '' : " #{usable_args.shelljoin}") |
||||
end |
||||
|
||||
def run **opts, &exe |
||||
opts = @opts.merge opts |
||||
case opts[:return] |
||||
when :lines |
||||
opts[:mode], exe = :io, lambda( &:readlines) |
||||
when :string |
||||
opts[:mode], exe = :io, lambda( &:read) |
||||
when :line |
||||
opts[:mode], exe = :io, lambda{|f|f.read.chomp} |
||||
end |
||||
r = f = fork **opts |
||||
begin |
||||
r = exe.call f if exe |
||||
rescue Object |
||||
f.kill |
||||
raise $! |
||||
ensure |
||||
f.close if f.respond_to? :close |
||||
f.wait |
||||
end |
||||
unless opts[:expect_status].nil? or opts[:expect_status] == f.exitstatus |
||||
raise ProcessError, "«#{self}» exited: #{f.status}" |
||||
end |
||||
r |
||||
end |
||||
|
||||
def run! **opts, &exe |
||||
run expect_status: 0, **opts, &exe |
||||
end |
||||
alias call run! |
||||
alias !@ run! |
||||
end |
||||
|
||||
attr_reader :opts |
||||
|
||||
def initialize dir = nil, **opts |
||||
opts = opts.merge dir: dir if dir |
||||
@opts = opts.dup |
||||
(@opts[:commands]||={}).each do |name, exe| |
||||
define_singleton_method name, &exe |
||||
end |
||||
@opts.freeze |
||||
end |
||||
|
||||
def chdir dir |
||||
Sh.new dir, **opts |
||||
end |
||||
alias cd chdir |
||||
def newopts **opts |
||||
Sh.new **@opts.merge( opts) |
||||
end |
||||
|
||||
def chroot dir |
||||
newopts chroot: dir |
||||
end |
||||
|
||||
def return_string |
||||
newopts mode: nil, return: :string |
||||
end |
||||
|
||||
alias _ return_string |
||||
def return_lines |
||||
newopts mode: nil, return: :lines |
||||
end |
||||
alias [] return_lines |
||||
|
||||
def with_io |
||||
newopts mode: :io |
||||
end |
||||
alias io with_io |
||||
|
||||
def system command, *args, immediately: nil, **opts, &exe |
||||
cmd = Command.new self, command, *args, **@opts.merge( opts) |
||||
if immediately or @opts[:immediately] or block_given? |
||||
cmd.run &exe |
||||
else |
||||
cmd |
||||
end |
||||
end |
||||
|
||||
def define_command name, &exe |
||||
@opts[:commands][name] = exe |
||||
define_singleton_method name, &exe |
||||
end |
||||
|
||||
def def_system_command name, cmd = nil |
||||
cmd ||= name |
||||
define_command name do |*args, **opts, &exe| |
||||
system cmd, *args, **opts, &exe |
||||
end |
||||
#e = <<-EOF |
||||
# def #{name}(*args, **opts, &exe) |
||||
# Command.new self, #{cmd.to_s.inspect}, *args, **opts |
||||
# end |
||||
#EOF |
||||
#eval e, nil, __FILE__, __LINE__ - 4 |
||||
end |
||||
|
||||
def def_system_commands *names |
||||
names.each {|cmd| def_system_command cmd } |
||||
end |
||||
|
||||
def alias_command name, cmd, *args, **opts, &exe |
||||
opts = @opts.merge opts |
||||
define_command name do |*as, **os, &e| |
||||
*as = exe.call( *as) if exe |
||||
system cmd, *args, *as, **opts, **os |
||||
end |
||||
end |
||||
end |
||||
|
||||
#f = Sh::IOFork.new do |
||||
# STDERR.puts f.inspect |
||||
# STDOUT.puts "hallo welt" |
||||
# sleep 5 |
||||
#end |
||||
# |
||||
#STDERR.puts "> #{f.inspect}" |
||||
#f.each_line do |l| |
||||
# STDERR.puts "> #{l}" |
||||
#end |
||||
#f.wait |
||||
#exit 1 |
@ -0,0 +1,49 @@ |
||||
require 'to_lvm_xfs/exts' |
||||
|
||||
class Image |
||||
attr_reader :base, :dir, :image, :root |
||||
def initialize base, dir, image: nil, root: nil |
||||
@base = base |
||||
@dir = |
||||
case dir |
||||
when Pathname then dir |
||||
when String then Pathname.new dir |
||||
else raise ArgumentError, "Pathname for dir expected" |
||||
end |
||||
@image = |
||||
case image |
||||
when Pathname then image |
||||
when String then Pathname.new image |
||||
when nil then @dir+'image' |
||||
else raise ArgumentError, "Pathname for image expected" |
||||
end |
||||
@image = @image.readlink.expand_path @image.expand_path.dirname while @image.symlink? |
||||
@root = |
||||
case root |
||||
when Pathname then root |
||||
when String then Pathname.new root |
||||
when nil then @dir+'root' |
||||
else raise ArgumentError, "Path for root must be Pathname, String or MP, if given" |
||||
end |
||||
end |
||||
def to_s() @dir.to_s end |
||||
|
||||
def kpartx() @base.kpartx @image end |
||||
def departx() @base.departx @image end |
||||
def lpartx() @base.lpartx @image end |
||||
def kpartxed?() @base.kpartxed? @image end |
||||
end |
||||
|
||||
class MP |
||||
attr_reader :base, :dev, :mp, :opts |
||||
def initialize base, dev, mp, *opts |
||||
@base, @dev, @mp, opts = base, dev, mp, opts |
||||
end |
||||
|
||||
def mount() @base.mount dev, mp, *opts end |
||||
def umount() @base.umount mp end |
||||
def uuid() @uuid ||= @base.sh.fs_uuid @dev end |
||||
def fstype() @fstype ||= @base.sh.fs_type @dev end |
||||
def clear_cache() @uuid = @fstype = nil end |
||||
end |
||||
|
@ -0,0 +1,7 @@ |
||||
#!/usr/bin/env ruby |
||||
|
||||
require 'pathname' |
||||
$: << Pathname.new($0).expand_path.dirname+'lib' |
||||
require 'to_lvm_xfs/raspbian' |
||||
|
||||
Raspian.new(*ARGV).run |
Loading…
Reference in new issue