#!/usr/bin/perl -w # 作者:洪朝貴 # URL: http://people.ofset.org/~ckhung/p/mk-boot-usb/ # 授權: GPL 3.0 或 更晚的版本 # 版本: mk-boot-usb-08h # 註解中文翻譯: 丁丁 use Getopt::Std; use strict; my ( %opts, $source_map, $SFD, # 諸操作參數 #. # 目的碟諸分割的複製來源掛載點 # 目的碟諸分割大小、檔案系統種類、是否開機 ); %opts = ( h => undef, # 印出幫助文件後離開 b => undef, # 批次模式? 僅設有 -d 且 [-z or -s] 有效 # 當 -v <= 1 時本程式會執行批次模式 v => 5, # 囉唆度 0-5,0不問任何問題,5最囉唆 d => undef, # 目的裝置,如 /dev/sdy <== 依實際情況 # 如果你指示錯裝置,電腦將無法開機 # # 僅當 -b 時才有作用 s => undef, # 來源裝置,如 /dev/sdx (用於複製時) z => undef, # 目的碟諸分割大小列表(單位 MB)以空白間開 # 如 '212 60 240' 注意:諸值合計須等於被建議大小 # (約為目的隨身碟大小扣除第4分割大小,單位MB) # 4 => 16, # 第4分割大小,至少放16MB給開機檔案 # 當複製時忽略-4(也就是當 -s 出現時) # -s 和 -z 互斥 m => undef, # 目的碟諸分割複製來源指示(僅-s有意義): # 從這個掛載點而不是來源分割複製 # 如 -m '1:/mnt/test,7:/live_media' 將 # 從 /mnt/test 複製到 /dev/sdy1取代從 /dev/sdx1; # 從 /live_media 複製到 /dev/sdy7 取代從 /dev/sdx7 p => "/usr/share/mk-boot-usb/", # 原目錄 w => "/tmp/mk-boot-usb/", # 工作目錄 ); # 所以為了避免囉唆的問題您會以批次模式使用本程式, # 您會下如下的命令列: # mk-boot-usb -b -v 1 -d /dev/sd-dst -z '212 60 240' # 或以批次模式複製,命令列如: # mk-boot-usb -b -v 1 -d /dev/sd-dst -s /dev/sds # /dev/sd-dst 是目標裝置 /dev/sds 是來源裝置 # getopts('hbv:d:s:z:4:km:p:w:', \%opts); # 取得參數 if ($opts{h}) { # 參數h,印幫助文件 print_help(); exit; } die unless defined $opts{4}; # 缺分割4離開,分割4有預設值 die "-4 must be followed by a number" if $opts{4} =~ /\D/; # 分割4非數字離開 die "$opts{4} MB is not enough for partition 4" if $opts{4} < 16; # 分割4小於16離開 # 備妥目的裝置資料 $opts{d} = ask_dev() unless ($opts{d} and $opts{b}); # 除非批次執行且給了目的裝置,否則以 ask_dev() 查出目的裝置 list_files($opts{d}) if $opts{v} >= 2; # 囉唆度2-5,印出目的碟中各分割最近五個檔案,以供判斷目的碟可否格式化 my (@a) = `sfdisk -s $opts{d}`; # 查出目的碟容量有幾k $SFD->[0]{size}{k} = $a[0]; # 容量以k表達 $SFD->[0]{size}{m} = k2m($SFD->[0]{size}{k}); # 容量以M表達 $SFD->[0]{size}{c} = floor(k2c($SFD->[0]{size}{k})); # 容量以磁柱數表達 # 計算目的裝置還剩多少MB die "=+=+= cowardly refuse to process huge destination usb stick ($SFD->[0]{size}{m} MB)" if $SFD->[0]{size}{m} > 36000; # 目的裝置超過32G就離開,以免format到硬碟 if ($opts{v} > 0) { # 囉唆度非0,提示扣除第4分割還有多少MB print "=+=+= size of destination stick $opts{d}: $SFD->[0]{size}{m} MB\n"; printf "=+=+= partition 4 using %d MB; %d MB available for other partitions\n", $opts{4}, $SFD->[0]{size}{m} - $opts{4} if $opts{4} > 0; } # 建工作目錄 system("mkdir -p $opts{w}"); # 準備分割參數 if ($opts{s}) { # 從來源裝置複製決定分割大小 die "=+=+= both -z and -c are specified. Don't know what to do." if $opts{z}; # 同時指示-s -z則離開,兩者互斥 fill_SFD_from_src($opts{s}); # 由源裝置去算目的裝置各分割大小及檔案系統種類 mount_all_src_parts($opts{s}); # 源裝置全部掛載、並算第1分割用量 my ($n, $need, $avail); $avail = k2m(c2k($SFD->[0]{size}{c} - $SFD->[0]{need_cyls})); # 扣除必要空間剩多少MB $SFD->[0]{need_cyls} += ceiling(k2c($SFD->[1]{usage})) + 1; # 必要磁柱數要再加第1分割中已使用部分還要再加1磁柱 $need = k2m($SFD->[1]{usage}); # 第1分割用量換算成k die "=+=+= $opts{s}1 requires $need MB but only $avail MB can be allocated on $opts{d}1" if ($SFD->[0]{need_cyls} > $SFD->[0]{size}{c}); # 如果目的裝置磁柱數小於必要磁柱數則離開 } else { # 使用者決定分割大小 my ($sum) = ask_part_sizes($SFD->[0]{size}{m},$opts{4},$opts{z}); # 由使用者分割大小指示去算出諸 $SFD ,並傳回合計MB die "=+=+= partition sizes add up to $sum, which is different from $SFD->[0]{size}{m} - $opts{4}" if $sum != $SFD->[0]{size}{m} - $opts{4}; # 目的裝置減第4分割大小不等於使用者指定諸分割合計MB則離開 } # 分割前安全確認 obtain_agreement($opts{d}) unless ($opts{b} and $opts{v} <= 1); # 最重要的工作 sfdisk($opts{d}); # 分割並格式化 install_minimal($opts{d}, $SFD->[0]{boot_part}, $opts{m}); # 複製 grub,ttylinux 兩目錄到開機分割之下 install_grub($opts{d}, $SFD->[0]{boot_part}); # 安裝grub到開機分割,即改寫裝置的MBR,使其由grub管理開機 # 複製 if ($opts{m}) { # 如有拷貝目錄指示,依其指示修正 if ($opts{s}) { fill_source_map(); } else { print "=+=+= -m is meaningless without -s, hence ignored\n"; } } if ($opts{s}) { clone(); } # 進行拷貝 print qq# =+=+= If there was no error message and if qemu is installed in your system, =+=+= your may now type 'qemu -usb $opts{d}' to test your bootable stick. # if $opts{v} >= 2; ############################################################ sub print_help { print qq# =+=+= This program _might_ wipe out your entire harddisk, and it =+=+= surely will wipe out your usb stick! =+=+= Make sure that all of your data on the usb stick has been =+=+= properly backed up elsewhere! =+=+= =+=+= In the interactive mode (the default), you will be prompted =+=+= a few questions. Do _not_ insert the usb stick that you want to =+=+= make bootable _until_ you are asked to do so. If it is already =+=+= attached to the computer now, please remove it, wait for a few =+=+= seconds, and run the program again. =+=+= Only the courageous (those who have backed up not only the data =+=+= on the usb stick, but also have backed up the entire harddisk =+=+= and are prepared to handle a _non-bootable_ computer if necessary) =+=+= are allowed to operate this program in non-interactive mode. =+=+= Please read the comments in the %opts part at the beginning of =+=+= this program for suitable command line options. #; } sub ask_dev { my (@devlist, $user_response, %devhash); @devlist = `ls /dev/sd?`; # 第一次列出諸usb裝置, print qq# =+=+= Insert the usb stick, wait a few seconds (often the usb stick =+=+= will flash a bit), and press enter to continue. If file manager(s) =+=+= pops up, please press control-C to abort and read the web pages =+=+= about disabling automatic mounting. #; $user_response = ; # 使用者動作 %devhash = map { $_=>1 } `ls /dev/sd?`; # 第二次列出諸usb裝置 delete @devhash{@devlist}; # 減去第一次的結果 @devlist = keys %devhash; if ($#devlist < 0) { # 如果數目小於0 die "=+=+= no newly inserted usb stick is detected.\nquitting "; } elsif ($#devlist > 1) { # 如果數目大於1 die "=+=+= too many newly inserted usb sticks: @devlist.\nquitting "; } chomp $devlist[0]; return $devlist[0]; # 傳回目的裝置 } sub list_files { my ($dev) = @_; my (@partlist, $part, $mount_point); @partlist = map { chomp; $_; } `ls ${dev}*`; # 如目標為/dev/sdb 取得 /dev/sdb /dev/sdb1…,去最後字元(空白) print "=+=+= Your usb stick has these partitions: @partlist\n"; print "=+=+= The most recently modified few files in each partition are:\n"; foreach $part (@partlist) { $mount_point = mount($part, "-o ro"); # 諸分割以唯讀掛載 print "=+=+= [ $part ]\n"; system( qq( ls -tlr $mount_point | tail -5 umount $mount_point ) ); # 列出各掛載目錄最近五個檔案,然後卸載 print "\n"; } } sub round { return int($_[0]+0.5); } # 進位 sub floor { return int($_[0]); } # 捨去 sub ceiling { # 四捨五入 my ($x) = @_; my ($L) = int($x+2); return $L - int($L - $x); } sub k2m { return round($_[0]/1024.0); } # kb轉MB sub m2k { return $_[0]*1024; } # MB轉kb sub k2c { return $_[0]*2.0/255/63; } # kb轉磁柱數 sub c2k { return $_[0]*255*63/2.0; } # 磁柱數轉kb,不進位 sub m2c { return k2c(m2k($_[0])); } # MB轉磁柱數,不進位 sub c2m { return k2m(c2k($_[0])); } # 磁柱數轉MB,不進位 sub fill_SFD_from_src { my ($dev) = @_; my ($line, @sizes, $pri); $SFD->[0]{need_cyls} = 0; # 除了第1分割以外,估計其餘分割所需的磁柱數 foreach $line (`sfdisk -l $dev`) { my ($i, $boot, $cyls, $blocks, $id) = $line =~ m|/dev/sd.(..)(....)\s+\S+\s+\S+\s+(\d+)\S*\s+(\d+)\S*\s+(\S+)\s+(\S+)|; # 找/dev/sd.(幾)(四個字元) 空白 非空白 空白 非空白 空白 (數字加非空白) 空白 (非空白) 空白 (非空白),其中()的部分派入 $line,注意正規表示式擷取6個,而$line只有前5個元素有變數名,所以最後一個沒有變數名 next unless defined $id and ($id =~ /^[5bce]$/ or $id eq '83'); # 忽略掉 id 非為 5(延伸) b,c(FAT32),e,83(Linux) # 只有延伸分割, vfat, ext2/3 才進行 # 忽略其他種類的分割 $i =~ s/\s//g; # 把空白全部換成空字串(去掉) $SFD->[$i] = { cyls => $cyls, fstype => "0x$id" }; # 當依序使用sfdisk分割格式化時 # 我們設定使用每磁柱 H255 S63 的規格 # $SFD->[0]{boot_part} = $i if ($boot =~ /\*/); # 如果 $boot 中找到 * ,將該分割的編號派給 $SFD->[0]{boot_part} # 如果來源碟有多個可開機分割,最後的可開機分割 # 將會佔據本變數值,其他的就淹滅掉了 $SFD->[0]{need_cyls} += $cyls if ($i>=2 and $i<=4); # 將2至4分割的磁柱數加起來 } $SFD->[1]{cyls} = $SFD->[0]{size}{c} - $SFD->[0]{need_cyls}; # 將目的碟大小減去2-4分割的磁柱數,即得第1分割的大小 $SFD->[0]{boot_part} = $SFD->[4]{cyls}>0 ? 4 : 1 unless $SFD->[0]{boot_part}; # 若源碟沒有開機分割,若第4分割大於0就以第4分割開機,否則就以第1分割開機 } sub mount_all_src_parts { my ($src) = (@_); my ($i, $part, $size, $usage); for ($i=1; $i<=$#$SFD; ++$i) { # 從第1分割做到最後分割 # $src = ( $source_map{$i} or mount("$opts{s}$i", "-o ro") ); next unless $SFD->[$i] and $SFD->[$i]{cyls} > 0; # 只做磁柱數大於0的分割 $source_map->[$i]{dir} = mount("$opts{s}$i", "-o ro") unless $source_map->[$i]{dir}; # 將分割掛載並將掛載點放 $source_map } foreach (`df`) { # 列出所有檔案系統的名稱、區段、用量 ($part, $size, $usage) = split " "; ($i) = $part =~ /$opts{s}(\d+)/; # 找名稱中含源裝置者 if (defined $i and $i eq '1') { $SFD->[1]{usage} = $usage; # 再找其中分割編號為1者,其用量值放 $SFD->[1]{usage} last; # 找到就跳出迴圈 } } } sub mount { # 掛載 $part my ($part, $mount_options) = @_; $mount_options = "" unless defined $mount_options; # 如沒有動作參數,動作參數設為空白 my (@mount_line, $mount_point, $t); @mount_line = `mount | grep '$part '`; # 查看己掛載的fs,找出其中含 '$part '者 ($t) = $part =~ m|([^/]+)$|; # 找 $part中,最後一段非/的字串,找到放 $t $mount_point = "$opts{w}/mnt/$t"; # 預設掛載點設在工作目錄之下的 /mnt/$t $mount_point =~ s#/+#/#g; # 所有多個/如「//」置換為「/」 if ($#mount_line >= 0) { # 如果從已掛載fs曾找到 $part $mount_line[0] =~ /$part +on +(\S+)/; # 找「裝置 on 掛載點」 die unless $1; # 找不到就離開 return $1; # 傳回掛載點 } # 上述if必不成立,才會做以下三行,不然已經return system("mkdir -p $mount_point") unless -d $mount_point; # 測試掛載點是不是目錄,如不是建立掛載點目錄 system("mount $mount_options $part $mount_point"); # 依掛載動作參數將fs表掛到掛載目錄 return $mount_point; # 傳回已掛載的掛載點目錄 } # sub umount { # my ($part) = @_; # system("umount $part"); # } sub ask_part_sizes { my ($total, $fourth, $size_str) = @_; if (not $size_str) { # 如無指定分割大小字串,問使用者要 my ($available, $a); $available = $total - $fourth; # 目的裝置大小減第4分割大小得可用大小 $a = $available - 300; # 可用大小減 300MB 得第1分割大小 printf qq# =+=+= Type in a list of numbers separated by spaces representing =+=+= the sizes of each partition in MB. For example: =+=+= $a 60 240 =+=+= means 1 fat and 2 ext2 partitions, each of size $a, 60, 240 MB. =+=+= Sizes: #; $size_str = ; } my (@size, $sum, $i); @size = split " ", $size_str; # 將指定分割大小字串以空白拆開後放陣列 @size=(undef,$size[0],undef,0,$fourth,@size[1..$#size]);# 調整陣列使輸入值分別對應註標1,5,6…,而第4分割大小對應註標4 $sum = 0; $SFD->[2] = { cyls => 0, fstype => '0x5' }; # 第二分割定為延伸分割,大小磁柱數初值為0 for ($i=4; $i<=$#size; ++$i) { # 做第4分割及其以後諸分割 $SFD->[$i] = { cyls => round(m2c($size[$i])), fstype => '0x83' }; # 諸分割算磁柱數,定為Linux分割 if ($i > 4) { $sum += $size[$i]; # 累加延伸分割內的 MB 數 $SFD->[2]{cyls} += $SFD->[$i]{cyls}; # 累加延伸分割內的磁柱數 } }; $SFD->[1] = { cyls => $SFD->[0]{size}{c} - $SFD->[2]{cyls} - $SFD->[4]{cyls}, fstype => '0xc' }; # 第1分割的磁柱數為目的碟總磁柱數減延伸分割磁柱數及第4分割磁柱數,定為 FAT Cyl > 1024 $SFD->[0]{boot_part} = $fourth>0 ? 4 : 1; # 第4分割大小非0,定為開機分割;否則以第1分割為開機分割 return $sum+$size[1]; # 傳回延伸分割加第1分割 MB 數,不含第4分割 MB 數 } sub obtain_agreement { # 使用者按「yes」回主程式,否則離開 my ($dev) = @_; my ($user_response); print qq# =+=+= THESE FILES and ALL OTHER FILES on $dev will be destroyed! =+=+= Press 'yes' to continue. Are you absolutely sure?#; $user_response = ; chomp $user_response; return if ($user_response =~ m/^\s*yes\s*$/i); print "=+=+= It's good to be cautious. Prepare well and come back.\n"; exit; } sub sfdisk { my ($dev) = @_; my ($i, $sfdisk_input, $mkdosfs); ($mkdosfs) = `which mkdosfs`; # 找出mkdosfs的路徑檔名 chomp $mkdosfs; if (not $mkdosfs) { ($mkdosfs) = `which mkfs.vfat`; # 或找出mkfs.vat的路徑檔名 chomp $mkdosfs; die "cannot find 'mkdosfs' or 'mkfs.vfat' command." unless $mkdosfs; # 兩個都找不到即離開 } $mkdosfs .= " -F 32" if (c2m($SFD->[1]{cyls}) > 1600); # 第1分割大於1.6G使用FAT32 $sfdisk_input = "$opts{w}/sfdisk.in"; # 準備將諸分割的磁柱數、種類、是否開機寫入 sfdisk.in open F, "> $sfdisk_input"; # 開檔輸出送往 sfdisk.in for ($i=1; $i<=$#$SFD; ++$i) { # 從第1分割寫到最後 if (defined $SFD->[$i] and $SFD->[$i]{cyls} > 0) { # 非空分割 print F ",$SFD->[$i]{cyls},$SFD->[$i]{fstype}"; # 寫入「,磁柱數,檔案系統類別」 print F ",*" if $SFD->[0]{boot_part} == $i; # 如果是開機分割寫入「,*」 print F "\n"; # 換行 } else { # 空分割 print F ",0\n"; # 「,0 換行」 } } close F; system(qq( dd count=16065 < /dev/zero > $dev dd < $opts{p}/lilo.mbr > $dev sfdisk -q -D -C $SFD->[0]{size}{c} -H 255 -S 63 $dev < $sfdisk_input echo '=+=+= formatting ${dev}1 ...' $mkdosfs ${dev}1 ) ); # 從空裝置抄寫1磁柱(16065磁區)到目的碟→lilo.mbr(512byte)抄入目的碟→將 sfdisk.in 資料抄到分割表→格式化第1分割 for ($i=4; $i<=$#$SFD; ++$i) { # format 第4分割以後的非0分割 next unless ($SFD->[$i] and $SFD->[$i]{cyls} > 0); print "=+=+= formatting $dev$i ...\n"; system("mkfs -t ext2 -I 128 -q $dev$i"); # 逐一將分割格式化為ext2 # 如果不下-I 128, slax的mkfs.ext2預設為 -I 256 # 結果會使 grub 在第二階段產生如下錯誤: # grub bad file or directory type } } sub fill_source_map { my (@map, $m); @map = split /,/, $opts{m}; # 將拷貝目錄指示用「,」分開成陣列,每元素是一條指示 foreach $m (@map) { if ($m =~ /(\d+):(.*)/) { # 找指示中「數字:任意字串」,以數字(第幾分割)為索引,分別產生 $source_map->[第幾分割]{dir}、$source_map->[第幾分割]{map} $source_map->[$1] = { dir => $2, map => 1 }; } else { # 指示格式不對,印錯誤訊息,並忽略指示。 print "=+=+= part of unparsed arg to -m ignored: '$m'\n"; } } } sub install_minimal { my ($dev, $boot_part, $minimal) = @_; my ($dev_part) = $dev . $boot_part; # 裝置加上開機分割編號,得到開機分割名稱 my ($mount_point) = mount($dev_part); # 掛載開機分割並傳回掛載點 print "=+=+= copying ttylinux and customized grub config files to $dev_part ...\n"; # including menu.lst and splash.xpm system(qq( cp -r --preserve=timestamps $opts{p}/boot/ $mount_point sync umount $mount_point ) ); # 複製 grub,ttylinux 兩目錄到開機分割之下→強迫將已改變的磁區寫入磁碟→卸載 } sub install_grub { my ($dev, $boot_part) = @_; my ($dev_part, $dev_map, @a, $grub_dev, $mount_point); $dev_part = $dev . $boot_part; # 裝置加上開機分割編號,得到開機分割名稱 $dev_map = "$opts{w}/device.map"; # 設備對映檔的檔名 print "=+=+= installing grub to $dev_part ...\n"; system("rm -f $dev_map"); # 強制刪除前一次作業產生工作目錄中的設備對映檔 system("echo | grub --batch --device-map=$dev_map"); # 產生目前的設備對映檔 @a = `grep $dev $dev_map`; # 找出含目的裝置的那一行 die if $#a != 0; # 找不到就離開 ($grub_dev) = $a[0] =~ /\((\S+)\)\s+$dev/; # 比對該行應為「(非空白) 空白 目的裝置」取括號中字為 $grub_dev(開機裝置代碼) my ($i) = $SFD->[0]{boot_part} - 1; # 開機裝置代碼的表示法中,分割編號從0算起,所以永遠比一般的分割編號少1。 $grub_dev = "($grub_dev,$i)"; # 開機分割的裝置代碼表示法 $mount_point = mount("$dev_part"); # 掛載開機分割 system(qq( grub-install --recheck --root-directory=$mount_point '$grub_dev' chmod -R a-w $mount_point/boot umount $mount_point dd count=1 seek=1 < /dev/zero > $dev ) ); # 安裝grub到MBR及開機分割→調開機分割boot目錄之屬性→卸載→從空裝置跳過1磁區抄1磁區入目的碟 } sub clone { # copy files for all partitions print "=+=+= Start copying... (It may take from a few minutes up to half an hour)\n"; my ($i, $src, $dst); for ($i=1; $i<=$#$SFD; ++$i) { # 做所有的分割 next unless (defined $SFD->[$i] and ($SFD->[$i]{fstype}=~/^0x[bce]$/ or $SFD->[$i]{fstype} eq '0x83') );# 只做檔案系統為FAT或Linux者 if ($SFD->[$i]{fstype} eq '0x83') { # 只針對Linux分割 # copy ext2 label my $label = `e2label $opts{s}$i`; # 找出源分割的label chomp $label; # 去尾 system("e2label $opts{d}$i $label"); # 抄入目的分割 } $src = $source_map->[$i]{dir}; # 找出要該分割要複製的源目錄 $dst = mount("$opts{d}$i"); # 掛載目的分割 # Don't touch files installed by grub! system(qq( cd $dst/boot/grub mkdir 000-do-not-touch-me mv *stage* 000-do-not-touch-me ) ) if $i == $SFD->[0]{boot_part}; # 如果是開機分割,把檔名含 stage 的諸檔保護起來 print "\n=+=+= copying from:\n"; if ($source_map->[$i]) { # 如有該分割來源資料夾掛載點,列出目錄中的總用量 system("du -s $src"); } else { system("df $src"); # 如未有掛載點,列出檔案系統的總用量 } print "=+=+= to:\n"; # make sure not to omit hidden files. system(qq( df $dst cd $src cp -a . $dst ) ); # 從來源目錄拷貝到目的目錄 system(qq( cd $dst/boot/grub mv 000-do-not-touch-me/* . rmdir 000-do-not-touch-me ) ) if $i == $SFD->[0]{boot_part}; # 如遇開機分割搬回檔名含 stage 的諸檔 system("umount $opts{d}$i"); # 卸載目的分割 # src is unmounted later. } }