Lane East 的 blog

一百年很短,一秒钟很长

slackware 包管理(pkgtools)分析 pkgtool

2011-02-03 04:37

pkgtool 是 pkgtools 包中较为(最为?)复杂的一个, 不光是功能复杂, 实现也复杂, 它有以下两种运行模式:

  • 命令行模式, 就是提供参数给 pkgtools, 不显示功能选择界面, 这种模式下, 只有软件安装的功能, 而没有“浏览”, “卸载”, “配置“功能
  • 交互模式, 直接运行 pkgtool, 这个时候使用 dialog 画出选择界面, 选择其中的功能来运行

交互模式下 pkgtool 除了安装软件之外, 还有“浏览”, “卸载”, “配置(也就是选择执行 $ADM_DIR/setup/setup.* 下面的一个配置脚本)“的功能, 此模式下的安装功能有三种位置可供选择:

  • 安装当前目录下的软件包
  • 从指定位置安装软件包
  • 从软盘安装软件包

在这三种位置中, 选择从软盘安装软件包时, SOURCE_MOUNTED 不设定, 所以会要求放入软盘

不管是命令行模式还是交互模式, pkgtools 的软件安装都为两种软件安装模式:

  • 普通模式, 它是直接安装一个目录下的软件包
  • 软件集模式, 它是安装某一个目录下的软件包, 并且在此目录下查找 install.end 文件, 如果此文件存在, 则表示一个软件集安装完成, 如果不存在, 则认为软件集存放在了不同的介质/目录, 将软件集名称后面加上编号, 继续安装, 比如安装软件集 a, 如果没找到 install.end, 则继续安装 a2, a3, 它通常有两种情况, 一种是 SOURCE_MOUNTED 未设定的情况, 这样的话会要求插入存储介质, 进行安装, 直到在某张盘中发现了 install.end, 就表示此软件集安装结束, 如果是安装多个软件集, 则还需要再次进行这样的操作, 另一个是 SOURCE_MOUNTED 已经设定成 always, 这时候认为软件包存放在一个目录($SOURCE_DIR)的子目录下, 同样, 如果这个子目录下没有 install.end, 则查找加了编号的子目录, 如安装 a 软件集, 则会先安装 $SOURCE_DIR/a 下的软件, 如果此目录下没有 install.end, 则会接着安装 $SOURCE_DIR/a2, $PACKAGE_DIR/a3 …如果是多个软件集, 则还要进行类似的操作

普通模式下, DISK_SETS 设置为 “disk”, 软件集模式下, DISK_SETS 设置为一个用 # 分割的列表, (理论上)不能有大写字母, 命令行模式的时候, 脚本会自动转换成小写字母, 而交互模式下的软盘安装时, 只是会提出要求, 并未有什么针对用户输入了大写字母的行动, 还有就是在 install_disk_set 函数的注释部分提到了这么一个要求

交互模式下的功能是搭配好了的, 选择了软盘安装, 则会要求选择一种软盘设备, 并且会提示放入软盘, 由本脚本来进行挂载, 但是在命令行模式下, 情况则自由了很多, 也复杂了很多, 因为可以用 -source_device 来指定设备, 所以就不一定是软盘设备了(但是挂载用的函数中使用了 msdos 文件系统来挂载), 甚至会出现指定了设备, 但是又没用到的情况(指定了设备, 同时又使用了 -source_mounted 参数)

另外, 在命令行模式下的软件集安装方式, 还可以用 $TMP/SeT* 进行额外设定:

  • SeTtagext: 此文件内容为 tagfile 的后缀, 如它的内容为 .txt 那么则用 tagfile.txt 为 tagfile
  • SeTtagpath: 此文件内容为 tagfile 的所在路径, 可以用来指定非标准的 tagfile 路径(标准的为 $SOURCE_DIR, 也就是所安装软件的软件包存放目录), 这个文件只有在没有 SeTtagext 的情况下起作用
  • SeTQUICK: 此文件内容不重要, 只要此文件存在, 则会尝试执行软件包存放目录中的 maketag 脚本, 再将此脚本产生的 SeTnewtag 存为 $TMP/tagfile, 以作为 tagfile 使用

中文注释的代码如下:

#!/bin/sh

SOURCE_DIR=/var/log/mount # 默认的软件包存放目录, 也是用来挂载设备的默认目录
ASK="tagfiles" # 默认用 tagfile 来判断是否安装
# 根据 /bin/chmod 和 /bin/chown 是否为符号链接来判断是否是在 rootdisk 下使用 busybox 的情况 {{{
if [ -L /bin/chmod -a -L /bin/chown ]; then
 # 用 rootdisk 的时候目标目录为 /mnt, 临时目录为 /mnt/var/log/setup/tmp
 TARGET_DIR=/mnt
 TMP=/mnt/var/log/setup/tmp
 if mount | grep "on /mnt" 1> /dev/null 2>&1 ; then
  true
 else
  # 如果 /mnt 上没挂载东西, 则给出提示, 并退出
  # 这是为了避免使用 rootdisk 维护系统时忘记挂载而导致的问题
  echo
  echo
  echo "You can't run pkgtool from the rootdisk until you've mounted your Linux"
  echo "partitions beneath /mnt. Here are some examples of this:"
  echo
  echo "If your root partition is /dev/hda1, and is using ext2fs, you would type:"
  echo "mount /dev/hda1 /mnt -t ext2"
  echo
  echo "Then, supposing your /usr partition is /dev/hda2, you must do this:"
  echo "mount /dev/hda2 /mnt/usr -t ext2"
  echo
  echo "Please mount your Linux partitions and then run pkgtool again."
  echo
  exit
 fi
else
 # 一般情况下运行 pkgtool, 目标目录为 /, 临时目录为 /var/log/setup/tmp
 TARGET_DIR=/
 TMP=/var/log/setup/tmp
fi
if [ ! -d $TMP ]; then
 mkdir -p $TMP
 chmod 700 $TMP
 fi
ADM_DIR=$TARGET_DIR/var/log
LOG=$TMP/PKGTOOL.REMOVED
# }}}

# 函数: 除去多余的空白 {{{
# 在 installpkg 中出现过
crunch() {
  while read FOO ; do
    echo $FOO
  done
}
# }}}

# 函数: 获取软件包的基础名称 {{{
# 将 foo-1.0.0-i386-1.tgz, foo-1.0.0-i386-1, foo.tgz, foo 等形式的名称统一转化为 foo
package_name() {
  # 此处用到了不同于 installpkg 中的获取包名称的方法, 更加简洁
  # 当然, 它仍然不能准确取得包含多于三个 "-" 的老式包名
  # 先将结尾的 .tgz 删除(其实这个地方应该用 s/\.tgz$//; 而不是 s/.tgz$//;)
  # 然后用替换的方式将尾部的的 -*-*-* 去除掉, 如果是旧式包名称, 那么没有匹配,
  # 也不会有问题.
  echo $1 | sed 's/.tgz$//;s/-[^-]*-[^-]*-[^-]*$//'
  return
  # 下面的部分是旧的获取软件名的方式, 因为上面的 return 不会被执行到
  STRING=`basename $1 .tgz`
  # Work out number of segments
  INDEX="`echo $STRING | tr -d -c -`"
  INDEX="`expr length $INDEX + 1`"
  # Check we have at least 4 segments
  if [ $INDEX -lt 4 ]; then
    # If we don't have four segments, return the old-style (or out of spec) package name:
    echo $STRING
  else # we have four or more segments, so we'll consider this a new-style name:
    NAME=`expr $INDEX - 3`
    NAME="`echo $STRING | cut -f 1-$NAME -d -`"
    echo $NAME
    # cruft for later ;)
    #VER=`expr $INDEX - 2`
    #VER="`echo $STRING | cut -f $VER -d -`"
    #ARCH=`expr $INDEX - 1`
    #ARCH="`echo $STRING | cut -f $ARCH -d -`"
    #BUILD="`echo $STRING | cut -f $INDEX -d -`"
  fi
}
# }}}

# 函数: 删除多个软件包 {{{
# 将传递进来的参数作为软件包列表, 逐个删除
remove_packages() {
 for pkg_name in $*
 do
  if [ -r $ADM_DIR/packages/$pkg_name ]; then
   # dialog 的 --cr-wrap 参数能使 dialog 命令中的文本中的换行显示成换行,
   # 所以下面的 \n\ 其实是可以不要的
   # 如果 installpkg 中的 dialog 使用 --cr-wrap 参数,
   # 那么也同样不需要用 paste 来再每一行尾部加 \n 的
   dialog --title "PACKAGE REMOVAL IN PROGRESS" --cr-wrap --infobox \
"\nRemoving package $pkg_name.\n\
\n\
Since each file must be checked \
against the contents of every other installed package to avoid wiping out \
areas of overlap, this process can take quite some time. If you'd like to \
watch the progress, flip over to another virtual console and type:\n\
\n\
tail -f $TMP/PKGTOOL.REMOVED\n" 13 60
   # 调用 removepkg 来删除软件包
   # removepkg 会根据环境变量 ROOT 来判断要操作的"根"目录
   # 用在诸如使用维护盘启动, 真正的根目录并非 / 的情况
   # 另, pkgtools 中的"根"目录判断方式不统一, removepkg 和 upgradepkg 中使用
   # ROOT 环境变量, installpkg 中使用 -root 参数, 如果没有这个参数, 再使用 ROOT 环境变量
   export ROOT=$TARGET_DIR
   removepkg $pkg_name >> $LOG 2> /dev/null
  else
   echo "No such package: $pkg_name. Can't remove." >> $LOG
  fi
 done
}
# }}}

# 参数处理
if [ $# -gt 0 ]; then
# 命令行模式运行 {{{
 while [ $# -gt 0 ]; do
  case "$1" in
  "-sets")            # 指定要安装的软件集名称
   DISK_SETS=`echo $2 | tr "[A-Z]" "[a-z]"` ; shift 2 ;; # 软件集名称要求小写
  "-source_mounted")  # 不需要挂载
   SOURCE_MOUNTED="always" ; shift 1 ;;
  "-ignore_tagfiles") # 安装时不询问是否安装, 不管 tagfiles 里的设定是什么
   ASK="never" ; shift 1 ;;
  "-tagfile")         # 指定 tagfile
   USETAG=$2 ; shift 2 ;;
  "-source_dir")      # 指定软件包所存放的位置
   SOURCE_DIR=$2 ; shift 2 ;;
  "-target_dir")      # 指定安装的目标位置
   TARGET_DIR=$2
   ADM_DIR=$TARGET_DIR/var/log
   shift 2 ;;
  "-source_device")   # 指定软件包存放的设备, 本脚本认为设备的文件系统是 msdos
   SOURCE_DEVICE=$2 ; shift 2 ;;
  esac
 done
# }}}
else
# 无参数, 交互式运行 {{{
 # 用 dialog 显示界面, 让用户进行选择
 # CMD_START 表示无参数运行 pkgtool, 这种情况下, pkgtool 会用 dialog 来画出界面,
 # 本脚本最后会根据 CMD_START 的情况, 来决定退出时是不是调用
 # dialog --clear 来清除屏幕上的对话框
 CMD_START="true"
 # 无参数, 以交互方式运行, 用不到 SeT*, 删除
 rm -f $TMP/SeT*
 while [ 0 ]; do
  # 功能选择界面 {{{
  # 产生 17x75 大小的窗口, 选项框高度为 7, 里面有如下选项:
  # Current  安装当前目录下的软件包
  # Other    从其它位置安装软件包(从一个已经存在的目录安装)
  # Floppy   从软盘安装软件包
  # Remove   卸载软件
  # View     浏览已安装的软件所安装的文件
  # Setup    设置(再次选择执行 slackware 提供的设置脚本)
  # Exit     退出
  # 并将所选的项存入 $TMP/reply 文件中
  dialog --title "Slackware Package Tool (pkgtool version 12.0)" \
--menu "\nWelcome to the Slackware package tool.\n\
\nWhich option would you like?\n" 17 75 7 \
"Current" "Install packages from the current directory" \
"Other" "Install packages from some other directory" \
"Floppy" "Install packages from floppy disks" \
"Remove" "Remove packages that are currently installed" \
"View" "View the list of files contained in a package" \
"Setup" "Choose Slackware installation scripts to run again" \
"Exit" "Exit Pkgtool" 2> $TMP/reply
  if [ ! $? = 0 ]; then # 选择了 Cancel(取消) 按钮
   rm -f $TMP/reply
   dialog --clear
   exit
  fi
  REPLY="`cat $TMP/reply`"
  rm -f $TMP/reply
  if [ "$REPLY" = "Exit" ]; then # 选择了 Exit(退出) 按钮
   dialog --clear
   exit
  fi # }}}
  if [ "$REPLY" = "Setup" ]; then # 功能: 设置 {{{
    # 选择了 Setup, 根据 slackware 提供的设置脚本而产生选择框
    # 将命令放入 $TMP/setupscr, 再加入后面根据 slackware 提供的脚本的名称来生成的内容,
    # 形成完整的命令, 再用 . $TMP/setupscr 执行

    # --checklist 建立一个复选列表, 后面的文本为说明
    # 17 70 9 表示 对话框尺寸为 17x70, 选择框高 9, 超过的可以通过上下键选择
    # 后面的部分为选择项, 如果没有 --item-help 参数, 则 3 个为一组, 分别为:
    # 选项名, 选项说明, 选项状态, 选项状态为 "on" 时项默认为选中, 其它为不选中
    # 有 --item-help 参数时 4 个为一组, 第 4 个参数在左下角显示作为提示
    echo 'dialog --title "SELECT SYSTEM SETUP SCRIPTS" --item-help --checklist \
    "Please use the spacebar to select the setup scripts to run.  Hit enter when you \
are done selecting to run the scripts." 17 70 9 \' > $TMP/setupscr
    for script in $ADM_DIR/setup/setup.* ; do
      # slackware 提供的脚本中有一行含有 #BLURB 的行
      # #BLURB= 后的内容显示了脚本的作用, 可以用作说明
      # 这里取出这个说明
      BLURB=`grep '#BLURB' $script | cut -b8-`
      # 对空说明进行处理, 便于后面统一处理
      if [ "$BLURB" = "" ]; then
        BLURB="\"\""
      fi
      # slackware 提供的设置脚本名结构为: setup.编号.脚本名称
      # 去掉脚本名称中的 setup 的部分, 并加入说明, 状态(不选中),
      # 以及左下角的说明(同说明)
      echo " \"`basename $script | cut -f2- -d .`\" $BLURB \"no\" $BLURB \\" >> $TMP/setupscr
    done
    # 将选择放入 $TMP/return
    echo "2> $TMP/return" >> $TMP/setupscr
    . $TMP/setupscr
    if [ ! "`cat $TMP/return`" = "" ]; then
      # 执行每个选中的脚本:
      for script in `cat $TMP/rturn` ; do
        # 将 $TMP/return 中的每行的内容重新变成包含对应的脚本名称
        scrpath=$ADM_DIR/setup/setup.`echo $script | tr -d \"` # tr 删出脚本名称外的引号
        # 获得根文件系统的设备名, 用以传递给要执行的设定脚本
        rootdevice="`mount | head -n 1 | cut -f 1 -d ' '`"
        # 执行设定脚本
        ( COLOR=on ; cd $TARGET_DIR ; . $scrpath / $rootdevice )
      done
    fi
    rm -f $TMP/return $TMP/setupscr
    continue
  fi # 功能: 设置 }}}
  if [ "$REPLY" = "View" ]; then # 功能: 查看 {{{
   # 选择了 View
   # 显示出已安装软件包的列表, 选择其中一项则显示所安装的文件

   # 默认项目, 查看一个特定软件所安装的文件返回后所选中的项, 目前为空
   DEFITEM=""
   export DEFITEM
   # 此处同样是将命令输出重定向到一个文件($TMP/viewscr)中, 然后用 . 来执行它
   (
     echo 'dialog $DEFITEM --item-help --menu "Please select the package you wish to view." 17 68 10 \'
     FILES=`ls $ADM_DIR/packages`
     if [ -n "$FILES" ]; then
       cd $ADM_DIR/packages
       # 从软件包安装记录中读取出软件包描述, 并用 sed 处理成需要的结果
       # 所用到的 grep 参数解释:
       #     -Z:  用二进制 00 来分隔查找到的文件名与查找的内容, 而不是默认的 :
       #     -H:  在查找到的结果前面加上对应的文件名
       #     -m1: 找到一个结果就不再查找
       #     -A1: 显示出查找出的内容之后的一行内容
       #
       # 所以下面的 grep 表示从所有包的记录文件中找到 PACKAGE DESCRIPTION 行和它
       # 的下一行(软件的简单说明), 并且在前面符加上记录文件的文件名, 且使用二进
       # 制 00 来分隔文件名和内容.
       # grep 会使用 -- 来分隔不同的文件的结果, 所以结果就是类似下面这样:
       #     aaa_base-12.0.0-noarch-1\x00PACKAGE DESCRIPTION:
       #     aaa_base-12.0.0-noarch-1\x00aaa_base: aaa_base (Basic Linux filesystem package)
       #     --
       #     aaa_elflibs-12.0.0-i486-3\x00PACKAGE DESCRIPTION:
       #     aaa_elflibs-12.0.0-i486-3\x00aaa_elflibs: aaa_elflibs (shared libraries needed by many programs)
       # \x00 表示二进制的 00
       # 由于最后一个结果的后面没有 --, 为了统一处理, 所以在后面加了个 echo; 形
       # 成三行一组的格式, 用于后面的 sed 的处理
       #
       # 后面的 sed 命令中 -n 参数表示不使用自动打印, 除非使用了 p 之类的命令
       # 除了常见的 s 命令之外, 下面的 sed 命令中还使用了 n, h, x 命令
       # 它们牵涉到 sed 的两个空间, 一个叫模式空间(pattern space),
       # 一个叫存储空间(hold space), 模式空间是用来存储当前行的内容的, 而存储空
       # 间是用来临时保存内容的:
       #     n   把下一行的内容读取到模式空间
       #     h   把模式空间的内容放入存储空间
       #     x   交换模式空间和存储空间的内容
       # sed 的命令使用换行或者是 ; 来分隔, 下面的 sed 命令用来处理 grep 产生的
       # 三行一组的内容, 用 aaa_base 的那一组为例:
       # h  把首行 aaa_base-12.0.0-noarch-1\x00PACKAGE DESCRIPTION: 放入存储空间
       # n  将下一行放入模式空间. 其实由于首行的部分对于我们来说是没有必要的, 所
       #    以第一个 h 命令是可以省略的.
       # /\x00/{h;n;};x;s/  */ /g  将含有 \x00 的行(现在是第二行)进行处理, {h;n;}
       #    是将两个命令合成一组, 表示模式空间的内容中如果有 \x00 的话, 就执行这
       #    两个命令, 如果没有, 就不执行, 其实由于前面要求 grep 使用 \x00 来分隔
       #    文件名和匹配的内容, 所以这里可以直接写 h;n, 这两个命令执行完之后, 
       #    模式空间的内容就是 --, 也就是三行一组的最后一行, 而存储空间中的内容
       #    是真正需要的内容:
       #    aaa_base-12.0.0-noarch-1\x00aaa_base: aaa_base (Basic Linux filesystem package)
       # x  将存储空间中需要的内容放入模式空间, 把模式空间的内容扔到存储空间中,
       #    这样就能开始真正的处理了
       # s/  */ /g  将连续的多个空间处理为一个空格
       # s/ $//  将行尾空格去掉(现在不存在连续的多个空格了)
       # s/[\"`$]/\\&/g  将有特殊含义的字符转义, 使其能正确输出, 这里的 & 表示
       #    前面的匹配内容
       # s/\(.*\)\x00\([^:]*:\)\? *\(.*\)/ "\1" "\3" "View information about package \1" \\/
       #    此命令中的 \(.*\)\x00 把二进制 00 前面的部分当作 \1, 后面的部分一直
       #    到 : 的部分是可选的, 所以使用 \?, 表示可以有也可以没有, 由于使用了括
       #    号, 所以 \2 是这部分内容, 后面的空格和*用来忽略空格, 剩下的部分其实
       #    是真正的软件包说明, 放入 \3. 所以在我们的例子中, 这一行是把
       #    aaa_base-12.0.0-noarch-1\x00aaa_base: aaa_base (Basic Linux filesystem package)
       #    变化为(下面三行为一行, 太长, 才折成三行写):
       #    "aaa_base-12.0.0-noarch-1" 
       #    "aaa_base (Basic Linux filesystem package)" 
       #    "View information about aaa_base-12.0.0-noarch-1" \
       # 这其实就是 dialog 的 --item-help --menu 参数所需要的一组内容:
       #   选项, 说明, 左下角的提示
       { grep '^PACKAGE DESCRIPTION:$' -Z -H -m1 -A1 $FILES; echo; } \
       | sed -n 'h;n;/\x00/{h;n;};x;s/  */ /g;s/ $//;s/[\"`$]/\\&/g
           s/\(.*\)\x00\([^:]*:\)\? *\(.*\)/ "\1" "\3" "View information about package \1" \\/;p'
     fi
     # 所选项放入 $TMP/return 中
     echo "2> $TMP/return"
   ) > $TMP/viewscr
   while [ 0 ]; do
    . $TMP/viewscr
    if [ ! "`cat $TMP/return`" = "" ]; then # 选择的项目不为空
     DEFITEM="--default-item `cat $TMP/return`"  # 设置默认项, 用于显示查看列表
                                                 # 由于前面形成的 $TMP/viewscr 中
                                                 # 使用了 $DEFITEM, 并且是在单引号
                                                 # 中的, 所以每次执行 $TMP/viewscr
                                                 # 的时候都会根据当前的 DEFITEM
                                                 # 设定来决定不同的默认项
     # 显示所安装的文件
     dialog --title "CONTENTS OF PACKAGE: `cat $TMP/return`" --no-shadow --textbox "$ADM_DIR/packages/`cat $TMP/return`" \
     0 0 2> /dev/null
    else # 没选择, 退出
     break
    fi
   done
   # 清除临时文件
   rm -f $TMP/return $TMP/viewscr $TMP/tmpmsg
   # 防止有问题的包改变 / 和 /tmp 的权限
   chmod 755 /
   chmod 1777 /tmp
   continue
  fi # 功能: 查看 }}}
  if [ "$REPLY" = "Remove" ]; then # 功能: 删除 {{{
  # 选择了 Remove
   (
     cat << EOF
dialog --title "SELECT PACKAGES TO REMOVE" --item-help --checklist \
"Please select the \
packages you wish to Remove. Use the \
spacebar to select packages to delete, and the UP/DOWN arrow keys to \
scroll up and down through the entire list." 20 75 11 \\
EOF
     # 下面的部分和 View 的部分类似, 只是改变了左下角提示
     # 并且因为一次可以删除多个软件, 所以使用了 --checklist 所以就多出个 off
     # 用来设定默认为不选择
     FILES=`ls $ADM_DIR/packages`
     if [ -n "$FILES" ]; then
       cd $ADM_DIR/packages
       { grep '^PACKAGE DESCRIPTION:$' -Z -H -m1 -A1 $FILES; echo; } \
       | sed -n 'h;n;/\x00/{h;n;};x;s/  */ /g;s/ $//;s/[\"`$]/\\&/g
           s/\(.*\)\x00\([^:]*:\)\? *\(.*\)/ "\1" "\3" off "Select\/Unselect removing package \1" \\/;p'
     fi
     echo "2> $TMP/return"
   ) > $TMP/rmscript
   # 删除上次的记录文件
   if [ -L $LOG -o -r $LOG ]; then
     rm -f $LOG
   fi
   # 生成一个空的记录文件
   cat /dev/null > $LOG
   chmod 600 $LOG
   chmod 700 $TMP/rmscript
   export ADM_DIR;
   $TMP/rmscript
   # 调用 remove_packages 来删除所有选中了的软件包
   # 其中的 tr -d "\042" 用来去掉选择的包名称外的引号, 在 $REPLY = "Setup" 中也有
   # 个类似的功能, 用的是 tr -d \"
   remove_packages `cat $TMP/return | tr -d "\042"`
   if [ "`cat $TMP/PKGTOOL.REMOVED`" = "" ]; then
    rm -f $TMP/PKGTOOL.REMOVED
    dialog --title "NO PACKAGES REMOVED" --msgbox "Hit OK to return \
to the main menu." 5 40
   else
    dialog --title "PACKAGE REMOVAL COMPLETE" --msgbox "The packages have \
been removed. A complete log of the files that were removed has been created \
in $TMP: PKGTOOL.REMOVED." 0 0
   fi
   rm -f $TMP/rmscript $TMP/return $TMP/tmpmsg $TMP/SeT*
   chmod 755 /
   chmod 1777 /tmp # 功能: 删除 }}}
  elif [ "$REPLY" = "Floppy" ]; then # 功能: 从软盘安装 {{{
   # 选择软盘设备
   dialog --title "SELECT FLOPPY DRIVE" --menu "Which floppy drive would \
you like to install from?" \
11 70 4 \
"/dev/fd0u1440" "1.44 MB first floppy drive" \
"/dev/fd1u1440" "1.44 MB second floppy drive" \
"/dev/fd0h1200" "1.2 MB first floppy drive" \
"/dev/fd1h1200" "1.2 MB second floppy drive" 2> $TMP/wdrive
   if [ $? = 1 ]; then
    dialog --clear
    exit # 用户选择了取消, 直接退出整个 pkgtool 命令
         # 这个地方我觉得用 continue 来返回功能选择界面更合适, 毕竟可以用户可能
         # 只是误选了功能而已
   fi
   SOURCE_DEVICE="`cat $TMP/wdrive`"
   rm -f $TMP/wdrive
   # 输入软件集的名称, 使用空格来分隔, 后面会把这个地方的空格处理成 #
   cat << EOF > $TMP/tmpmsg

Enter the names of any disk sets you would like to install.
Separate the sets with a space, like this: a b oi x

To install packages from one disk, hit [enter] without typing
anything.

EOF
   dialog --title "SOFTWARE SELECTION" --inputbox "`cat $TMP/tmpmsg`" 13 70 2> $TMP/sets 
   DISK_SETS="`cat $TMP/sets`"
   rm -f $TMP/sets
   if [ "$DISK_SETS" = "" ]; then # 未输入软件集, 将 DISK_SETS 设置为 disk
    DISK_SETS="disk"
   else
    # 将原先输入的用空格分割的内容用 # 来分割
    DISK_SETS=`echo $DISK_SETS | sed 's/ /#/g'`
    DISK_SETS="#$DISK_SETS" # 在开始添加上 #
   fi
   break; # 功能: 从软盘安装 }}}
  elif [ "$REPLY" = "Other" ]; then # 功能: 从指定位置安装 {{{
   dialog --title "SELECT SOURCE DIRECTORY" --inputbox "Please enter the name of the directory that you wish to \
install packages from:" 10 50 2> $TMP/pkgdir
   if [ $? = 1 ]; then
    rm -f $TMP/pkgdir $TMP/SeT*
    dialog --clear
    exit # 同软盘安装模式中一样, 用户选择了取消, 我觉得用 continue 更合理
   fi
   SOURCE_DIR="`cat $TMP/pkgdir`"
   SOURCE_MOUNTED="always" # 将 SOURCE_MOUNTED 设置为 always, mount_the_source
                           # 函数就不会再挂载安装介质了
   DISK_SETS="disk"
   chmod 755 $TARGET_DIR
   chmod 1777 $TARGET_DIR/tmp
   rm -f $TMP/pkgdir
   if [ ! -d $SOURCE_DIR ]; then
    dialog --title "DIRECTORY NOT FOUND" --msgbox "The directory you want to \
install from ($SOURCE_DIR) \
does not seem to exist. Please check the directory and then try again." \
10 50
    dialog --clear
    exit # 输入的路径不是一个目录, 则退出整个 pkgtool 命令
         # 同样, 我觉得用 continue 更合理, 因为用户可能是输入错误, 回到功能选择
         # 界面会更方便一些
   fi
   break; # 功能: 从指定位置安装 }}}
  else # 功能: 从当前目录安装 {{{
   SOURCE_MOUNTED="always" # 无需挂载
   SOURCE_DIR="$PWD"
   DISK_SETS="disk"
   chmod 755 $TARGET_DIR
   chmod 1777 $TARGET_DIR/tmp
   break;
  fi # 功能: 从当前目录安装 }}}
 done
# }}}
fi
if [ "$DISK_SETS" = "disk" ]; then # DISK_SETS 为 disk 的时候将 ASK 设置为 always
                                   # 意味着普通安装模式下会不停地问是否安装的, 真恐怖
 ASK="always"
fi

# 函数: 挂载软件存储介质 {{{
# 将 $SOURCE_DEVICE 挂载到 $SOURCE_DIR
mount_the_source() {
 if [ "$SOURCE_MOUNTED" = "always" ]; then # 不需要挂载
  # 检查目录是否存在
  if [ ! -d $SOURCE_DIR ]; then
   cat << EOF > $TMP/tmpmsg

Your source device cannot be accessed properly.

Please be sure that it is mounted on $SOURCE_DIR,
and that the Slackware disks are found in subdirectories 
of $SOURCE_DIR like specified.

EOF
   dialog --title "MOUNT ERROR" --msgbox "`cat $TMP/tmpmsg`" 11 67
   rm -f $TMP/tmpmsg
   exit 1;
  fi
  return 0;
 fi # End if [ "$SOURCE_MOUNTED" = "always" ]
 # 如果 SOURCE_MOUNTED 不为 always, 提示放入软盘
 dialog --title "INSERT DISK" --menu "Please insert disk $1 and \
press ENTER to continue." \
11 50 3 \
"Continue" "Continue with the installation" \
"Skip" "Skip the current disk series" \
"Quit" "Abort the installation process" 2> $TMP/reply
 if [ ! $? = 0 ]; then
  REPLY="Quit"
 else
  REPLY="`cat $TMP/reply`"
 fi
 rm -f $TMP/reply
 if [ "$REPLY" = "Skip" ]; then
  return 1;
 fi
 if [ "$REPLY" = "Quit" ]; then
   dialog --title "ABORTING" --msgbox "Aborting software installation." 5 50
   chmod 755 $TARGET_DIR
   chmod 1777 $TARGET_DIR/tmp
   exit 1;
 fi;
 # 挂载设备, 如果不成功则重试, 直到成功或者用户选择放弃
 go_on=y
 not_successfull_mounted=1
 while [ "$go_on" = y -a "$not_successfull_mounted" = 1 ]; do
  mount -r -t msdos $SOURCE_DEVICE $SOURCE_DIR
  not_successfull_mounted=$?
  if [ "$not_successfull_mounted" = 1 ]; then
   mount_answer=x
   while [ "$mount_answer" != "y" -a "$mount_answer" != "q" ] ; do
    dialog --title "MOUNT PROBLEM" --menu "Media was not successfully \
mounted! Do you want to \
retry, or quit?" 10 60 2 \
"Yes" "Try to mount the disk again" \
"No" "No, abort." 2> $TMP/mntans
    mount_answer="`cat $TMP/mntans`"
    rm -f $TMP/mntans
    if [ "$mount_answer" = "Yes" ]; then
     mount_answer="y"
    else
     mount_answer="q"
    fi
   done
   go_on=$mount_answer
  fi
 done
 test $not_successfull_mounted = 0
}
# }}}

# 函数: 卸载(umount)设备 {{{
# SOURCE_MOUNTED 为 always 的时候不卸载
umount_the_source() {
 if [ ! "$SOURCE_MOUNTED" = "always" ]; then
  umount $SOURCE_DEVICE 1> /dev/null 2>&1
 fi;
}
# }}}

# 函数: 安装 {{{
install_disk() {
 # 挂载安装介质, 如果无法挂载则退出(无需挂载, 并且软件包存放的目录正常时不退出)
 mount_the_source $1
 if [ $? = 1 ]; then
  umount_the_source;
  return 1;
 fi
 CURRENT_DISK_NAME="$1"
 PACKAGE_DIR=$SOURCE_DIR
 # 无需挂载, 并且是软件集安装方式的时候 PACKAGE_DIR 设定为 $PACKAGE_DIR/$1
 if [ "$SOURCE_MOUNTED" = "always" -a ! "$DISK_SETS" = "disk" ]; then
   PACKAGE_DIR=$PACKAGE_DIR/$1
 fi

 # 目录不存在或者是目录中没有 .tgz 文件, 退出 {{{
 # 这个返回值会影响到软件集的安装, 如果用户只是插错了软盘, 由于这里的 return 1,
 # install_disk_set 会认为是软件集已经安装结束
 if [ ! -d $PACKAGE_DIR ]; then
  return 1
 fi
 if ls $PACKAGE_DIR/*.tgz 1> /dev/null 2> /dev/null ; then
  true
 else
  return 1
 fi
 # }}}

 # tagfile 的处理
 touch $TMP/tagfile
 if [ ! "$DISK_SETS" = "disk" ]; then # 软件集安装模式
  if [ -r $TMP/SeTtagext ]; then
   # 有 SeTtagext, 使用其内容为 tagfile 文件名的后缀 {{{
   if [ -r $PACKAGE_DIR/tagfile`cat $TMP/SeTtagext` ]; then
    cat $PACKAGE_DIR/tagfile`cat $TMP/SeTtagext` >> $TMP/tagfile
   else # 加了后缀的 tagfile 不存在, 使用原始的名称
    if [ -r $PACKAGE_DIR/tagfile ]; then
     cat $PACKAGE_DIR/tagfile >> $TMP/tagfile
    fi
   fi
   # }}}
  elif [ -r $TMP/SeTtagpath ]; then
   # 使用 SeTtagpath 中设定的目录作为 tagfile 的存放目录 {{{
   custom_path=`cat $TMP/SeTtagpath`
   short_path=`basename $PACKAGE_DIR`

   # 设定的目录中有 tagfile, 使用它
   if [ -r $custom_path/$short_path/tagfile ]; then
    cat $custom_path/$short_path/tagfile >> $TMP/tagfile

   else # 指定目录中没有 tagfile, 使用原始位置的
    if [ -r $PACKAGE_DIR/tagfile ]; then
     cat $PACKAGE_DIR/tagfile >> $TMP/tagfile
    fi
   fi
   # }}}
  elif [ -r $PACKAGE_DIR/tagfile ]; then
   # 没相关设定, 直接使用原始位置原始文件的内容
   cat $PACKAGE_DIR/tagfile >> $TMP/tagfile
  fi

  # 命令行模式, 设定了 SeTQUICK, 进入 QUICK 模式 {{{
  # QUICK 模式: 使用脚本来生成 tagfile
  if [ -r $TMP/SeTQUICK -a -r $PACKAGE_DIR/maketag ]; then
   # MAKETAG 环境变量用来设定 maketag 脚本的名称, 如果此变量已设定
   # 并且 $PACKAGE_DIR/$MAKETAG 存在, 则执行该脚本
   # 否则才执行默认的 $PACKAGE_DIR/maketag
   if [ ! "$MAKETAG" = "" -a -r $PACKAGE_DIR/$MAKETAG ]; then # use alternate maketag
    sh $PACKAGE_DIR/$MAKETAG
   else
    sh $PACKAGE_DIR/maketag
   fi
   if [ -r $TMP/SeTnewtag ]; then
    mv $TMP/SeTnewtag $TMP/tagfile
   fi
  fi
  # }}}

  # 重设 tagfile 的权限, 防止恶意修改
  if [ -r $TMP/tagfile ]; then
   chmod 600 $TMP/tagfile
  fi

 fi #  ! "$DISK_SETS" = "disk"

 # 如果用户在命令行指定过 tagfile, 则把这个文件内容给覆盖到 $TMP/tagfile 里面
 # 真狠, 上面忙了半天就全部作废- -U
 if [ ! "$USETAG" = "" ]; then
   cat $USETAG > $TMP/tagfile
 fi

 # 如果存在目录(不是文件夹那种目录, 而是像书目那种目录)文件, 则根据这个目录文件来
 # 检查是否缺少包或者有多出来的包
 # 这个地方的检查其实很麻烦的, 因为它是对每一个包进行处理的
 # 也就是说, 少一个或者多一个就会弹出一次对话框,
 # 如果多或者少的包数量很大的话, 会很烦人的
 #
 # 这里用到的目录文件的格式不太清楚, 也没找到相关的东西, 所以不太好分析
 if [ "$1" = "single_disk" -o -r $PACKAGE_DIR/disk$1 -o -r $PACKAGE_DIR/package-list.txt ]; then
  if [ -r $PACKAGE_DIR/package-list.txt ]; then
   CATALOG_FILE=$PACKAGE_DIR/package-list.txt
  else
   CATALOG_FILE=`basename $PACKAGE_DIR/disk*`;
  fi
  if [ -r $PACKAGE_DIR/$CATALOG_FILE -a ! -d $PACKAGE_DIR/$CATALOG_FILE ]; then
   if grep CONTENTS: $PACKAGE_DIR/$CATALOG_FILE 1> /dev/null 2>&1 ; then
    # 检查是否缺少软件包 {{{
    for PKGTEST in `grep "^CONTENTS:" $PACKAGE_DIR/$CATALOG_FILE | cut -f2- -d : 2> /dev/null` ; do
     # 不是严格的检查, 比如缺少了 emacs, 但是却有 emacs-nox, 就会出现误判
     if ls $PACKAGE_DIR/$PKGTEST*.tgz 1> /dev/null 2> /dev/null ; then
      true
     else
      cat << EOF > $TMP/tmpmsg

WARNING!!!

While looking through your index file ($CATALOG_FILE),
I noticed that you might be missing a package:

$PKGTEST-\*-\*-\*.tgz

that is supposed to be on this disk (disk $1). You may go
on with the installation if you wish, but if this is a 
crucial file I'm making no promises that your machine will
boot.

EOF
      dialog --title "FILE MISSING FROM YOUR DISK" --msgbox \
"`cat $TMP/tmpmsg`" 17 67
     fi
    done # }}}
    # 检查是否有多出来的包 {{{
    ALLOWED="`grep CONTENTS: $PACKAGE_DIR/$CATALOG_FILE | cut -b10- 2> /dev/null`" 
    for PACKAGE_FILENAME in $PACKAGE_DIR/*.tgz; do
     BASE="`basename $PACKAGE_FILENAME .tgz`"
     BASE="`package_name $BASE`"
     if echo $ALLOWED | grep $BASE 1> /dev/null 2>&1 ; then
      true
     else
      cat << EOF > $TMP/tmpmsg

WARNING!!!

While looking through your index file ($CATALOG_FILE),
I noticed that you have this extra package:

($BASE.tgz) 

that I don't recognize. Please be sure this package is
really supposed to be here, and is not left over from an
old version of Slackware. Sometimes this can happen at the 
archive sites.

EOF
      dialog --title "EXTRA FILE FOUND ON YOUR DISK" \
--msgbox "`cat $TMP/tmpmsg`" 17 67 
      rm -f $TMP/tmpmsg
     fi
    done # }}}
   fi
  fi
 fi

 # 安装文件
 for PACKAGE_FILENAME in $PACKAGE_DIR/*.tgz; do
  # 如果 $PACKAGE_FILENAME 下没有 .tgz 文件, 则跳过
  # 既是跳过这一次, 也是跳过所有, 因为如果没有 .tgz 文件, 上面的 for .. in ..; do
  # 中只会有一项
  # 其实上面已经检查过目录下是否存在 .tgz 文件了
  if [ "$PACKAGE_FILENAME" = "$PACKAGE_DIR/*.tgz" ]; then
   continue;
  fi
  if [ "$ASK" = "never" ]; then # 不询问, 直接安装, 用 -infobox 作参数调用 installpkg
   installpkg -root $TARGET_DIR -infobox -tagfile $TMP/tagfile $PACKAGE_FILENAME
   ERROR=$?
  elif [ "$ASK" = "tagfiles" ]; then # 直接根据 tagfile 的设定来决定是否安装
   installpkg -root $TARGET_DIR -menu -tagfile $TMP/tagfile $PACKAGE_FILENAME
   ERROR=$?
  else # ASK 为 always, 除了使用 tagfile 之外, 还会询问是否安装
   installpkg -root $TARGET_DIR -menu -ask -tagfile $TMP/tagfile $PACKAGE_FILENAME
   ERROR=$?
  fi
  # installpkg 返回 99 表示用户在安装软件对话框上选择退出(Quit), 而不是取消(Cancel)
  # 退出安装过程, 剩余的软件包也不再安装, 对软件集的安装有同样的效果
  if [ "$ERROR" = "99" ]; then
   umount_the_source;
   chmod 755 $TARGET_DIR
   chmod 1777 $TARGET_DIR/tmp
   exit 1;
   fi
 done
 # 检查是否存在 install.end, 用来判断一个软件集是否安装完毕
 # 安装完毕则返回 1, install_disk_set 中会使用这个返回值
 OUTTAHERE="false"
 if [ -r $PACKAGE_DIR/install.end ]; then
  OUTTAHERE="true"
 fi
 umount_the_source; # 根据需要卸载设备
 if [ "$OUTTAHERE" = "true" ]; then
  return 1;
 fi
}
# }}}

# 函数: 安装软件集合 {{{
# 只接收一个参数, 要求是小写字母
install_disk_set() {
 SERIES_NAME=$1
 CURRENT_DISK_NUMBER="1";
 while [ 0 ]; do
  # 一个包集合不一定在一个盘上, 开始的一个盘不加数字后缀, 之后数字后缀递增
  # 用在软盘作为安装介质的时代的, 比如包集合 a, 第一张软盘上的目录是 a,
  # 之后分别是 a2, a3 之类的
  if [ $CURRENT_DISK_NUMBER = 1 ]; then
    DISKTOINSTALL=$SERIES_NAME
  else
    DISKTOINSTALL=$SERIES_NAME$CURRENT_DISK_NUMBER
  fi
  install_disk $DISKTOINSTALL
  # 当 install_disk 发现软件包存储目录下有 install.end 的时候, 表示这个软件集
  # 已经是最后一个盘了, 于是返回非 0, 表示软件集合安装完成
  if [ ! $? = 0 ]; then
   return 0;
  fi
  # 磁盘号递增
  CURRENT_DISK_NUMBER=`expr $CURRENT_DISK_NUMBER + 1`
 done;
}
# }}}

# /* main() */ ;)
if [ "$DISK_SETS" = "disk" ]; then # 非软件集安装模式
 install_disk single_disk;
 ASK="always"
else # 软件集安装模式 {{{
 touch $TMP/tagfile
 chmod 600 $TMP/tagfile
 # 如果 DISK_SETS 包含有 a 软件集, 则把 a 单独处理, 不明白为什么
 # 而且如果 DISK_SETS 里面 a 是最后一个的话, 是不会产生 #a# 这样的情况的,
 # 因为前面的部分是把用空格分割的软件集中的空格换成 #, 然后在开始加上 # 形成用
 # # 分割的 DISK_SETS, 但是并没有在最后加上 #, 这样的话, DISK_SETS 的最后是 #a,
 # 而不会出现 #a#
 if echo $DISK_SETS | grep "#a#" 1> /dev/null 2>&1; then
  A_IS_NEEDED="true"
 else
  A_IS_NEEDED="false"
 fi
 while [ 0 ];
 do
  while [ 0 ]; # 将 DISK_SETS 中开头部分的 # 去掉, 此处考虑了 多个 # 的情况
  do
   if [ "`echo $DISK_SETS | cut -b1`" = "#" ]; then
    DISK_SETS="`echo $DISK_SETS | cut -b2-`"
   else
    break;
   fi
  done
  # 如果前面的 DISK_SETS 中包含 a 软件集, 在此单独处理
  if [ "$A_IS_NEEDED" = "true" ]; then
   cat << EOF > $TMP/tmpmsg

--- Installing package series ==>a<==

EOF
   dialog --infobox "`cat $TMP/tmpmsg`" 5 45
   sleep 1
   rm -f $TMP/tmpmsg
   # a 后面的 ; 不需要, 难道本脚本的编写者是个 c 语言的开发者? :-)
   # 下面还有些类似的
   install_disk_set a;
   A_IS_NEEDED="false"
  fi
  count="1"
  if [ "`echo $DISK_SETS | cut -b$count`" = "" ]; then
   break; # DISK_SETS 的第一个字节为空了, 说明 DISK_SETS 已经被处理完了, 结束
  else
   # 这个地方是用 count 来计算从开始到下一个 # 或者是末尾有多少字节数,
   # 然后就可以用 cut -b1-$count 来得到 DISK_SETS 中下一个软件集的名称了
   # 这种处理方式并不算好, 完全可以用 cut -d# -f<N> 来获取 diskset(<N>为数字)
   # 只要用:
   #   sed 's/^#\{1,\}//;s/#\{2,\}/#/g'
   # 把开头的 # 去掉, 避免第一次 cut 获取到空字串, 再把多个连续的 # 改为单独的 #
   # 避免 cut 获取到空字串, 就能以 cut 获取到空字串为结束的标志了.
   # 这样代码应该会简单很多.
   count="2"
   while [ 0 ]; do
    if [ "`echo $DISK_SETS | cut -b$count`" = "" -o "`echo $DISK_SETS | cut -b$count`" = "#" ]; then
     count="`expr $count - 1`"
     break;
    else
     count="`expr $count + 1`"
    fi
   done
  fi
  # 将下一个要安装的软件集名称存放到 diskset 中
  diskset="`echo $DISK_SETS | cut -b1-$count`"
  # 把已经获取的软件集名称从 DISK_SETS 中去除,
  # 因为 cut -b$count- 是包含第 $count 的字符的, 所以要把 count 加上 1
  # 把上一个软件集的最后一个字母去掉
  count="`expr $count + 1`"
  DISK_SETS="`echo $DISK_SETS | cut -b$count-`"
  if [ "$diskset" = "a" ]; then
   continue; # 前面已经单独处理过 a 软件集了
  fi
  # 安装其它的软件集
  cat << EOF > $TMP/tmpmsg

Installing package series ==>$diskset<==

EOF
  dialog --infobox "`cat $TMP/tmpmsg`" 5 45
  sleep 1
  rm -f $TMP/tmpmsg
  install_disk_set $diskset;
 done
fi # }}}

# 清理
if [ "$DISK_SETS" = "disk" -o "$CMD_START" = "true" ]; then
 # 如果不是软件集的安装方式, 或者是交互方式安装的
 # 则清理 $TMP/tagfile
 # 并使用 dialog --clear 来清除由 dialog 而产生的对话框在屏幕上的残留
 # 这样的话, 不是意味着软件集的安装方式下 tagfile 不会被删除了?
 if [ -r $TMP/tagfile ]; then
  rm $TMP/tagfile
 fi
 dialog --clear
fi
chmod 755 $TARGET_DIR $TARGET_DIR/var $TARGET_DIR/usr
chmod 1777 $TARGET_DIR/tmp

# vim:fdm=marker:ft=sh:

注 pkgtool 的代码比较复杂, 所以在里面加上了 {{{ 和 }}} 这样的标记用来让 vim 进行代码折叠

分类:

评论

  预览后可提交