#!/usr/bin/env bash set -eu ###################################################################################### #-- wfa configs --# wfa_version="0.1" wfa_name="WFA" wfa_command="wfa" #-- options (str) --# aururl="https://aur.archlinux.org/" operation="none" #-- options (bool) --# debug=false force_aur=false msgdebug=false nocolor=false noconfirm=false nodeps=false nomakepkgconf=false #-- makepkg --# # 実行ファイル makepkg_command="/usr/bin/makepkg" # 設定ファイル makepkg_config="/etc/makepkg.conf" # 引数 makepkg_args="" #-- pacman --# # 実行ファイル pacman_command="/usr/bin/pacman" # 設定ファイル pacman_config="/etc/pacman.conf" # 引数 pacman_args="" #-- git --# # 実行ファイル git_command="/usr/bin/git" # 引数 git_args="" #-- gpg --# # 実行ファイル gpg_command="/usr/bin/gpg" # 引数 gpg_args="" ###################################################################################### # メッセージ出力の制御 # https://github.com/FascodeNet/alterlinux/blob/dev/tools/msg.sh の変数名にアンダーバーを追加し関数化 msg() { local OPTIND OPTARG arg local _appname="msg.sh" local _bash_debug=false local _nocolor=false local _echo_opts="" local _message="" local _msg_type="info" local _msg_label="" local _label_space="7" local _adjust_chr=" " local _customized_label=false local _customized_label_color=false local _nolabel=false local _noappname=false local _noadjust=false local _output="stdout" _help() { echo "usage ${0} [option] [type] [message]" echo echo "Display a message with a colored app name and message type label" echo echo " General type:" echo " info General message" echo " warn Warning message" echo " error Error message" echo " debug Debug message" echo echo " General options:" echo " -a [name] Specify the app name" echo " -c [character] Specify the character to adjust the label" echo " -l [label] Specify the label." echo " -n | --nocolor No output colored output" echo " -o [option] Specify echo options" echo " -r [color] Specify the color of label" echo " -s [number] Specifies the label space." echo " -x | --bash-debug Enables output bash debugging" echo " -h | --help This help message" echo echo " --nolabel Do not output label" echo " --noappname Do not output app name" echo " --noadjust Do not adjust the width of the label" } while getopts "a:c:l:no:r:s:xh-:" arg; do case ${arg} in a) _appname="${OPTARG}" ;; c) _adjust_chr="${OPTARG}" ;; l) _customized_label=true _msg_label="${OPTARG}" ;; n) _nocolor=true ;; o) _echo_opts="${OPTARG}" ;; r) _customized_label_color=true case ${OPTARG} in "black") _labelcolor="30" ;; "red") _labelcolor="31" ;; "green") _labelcolor="32" ;; "yellow") _labelcolor="33" ;; "blue") _labelcolor="34" ;; "magenta") _labelcolor="35" ;; "cyan") _labelcolor="36" ;; "white") _labelcolor="37" ;; *) return 1 ;; esac ;; s) _label_space="${OPTARG}" ;; x) _bash_debug=true set -xv ;; h) _help shift 1 exit 0 ;; -) case "${OPTARG}" in "nocolor") _nocolor=true ;; "bash-debug") _bash_debug=true set -xv ;; "help") _help exit 0 ;; "nolabel") _nolabel=true ;; "noappname") _noappname=true ;; "noadjust") _noadjust=true ;; *) _help exit 1 ;; esac esac done shift $((OPTIND - 1)) # Color echo # # Text Color # 30 => Black # 31 => Red # 32 => Green # 33 => Yellow # 34 => Blue # 35 => Magenta # 36 => Cyan # 37 => White # # Background color # 40 => Black # 41 => Red # 42 => Green # 43 => Yellow # 44 => Blue # 45 => Magenta # 46 => Cyan # 47 => White # # Text decoration # You can specify multiple decorations with ;. # 0 => All attributs off (ノーマル) # 1 => Bold on (太字) # 4 => Underscore (下線) # 5 => Blink on (点滅) # 7 => Reverse video on (色反転) # 8 => Concealed on case ${1} in "info") _msg_type="type" _output="stdout" [[ "${_customized_label_color}" = false ]] && _labelcolor="32" [[ "${_customized_label}" = false ]] && _msg_label="Info" shift 1 ;; "warn") _msg_type="warn" _output="stdout" [[ "${_customized_label_color}" = false ]] && _labelcolor="33" [[ "${_customized_label}" = false ]] && _msg_label="Warning" shift 1 ;; "debug") _msg_type="debug" _output="stdout" [[ "${_customized_label_color}" = false ]] && _labelcolor="35" [[ "${_customized_label}" = false ]] && _msg_label="Debug" shift 1 ;; "error") _msg_type="error" _output="stderr" [[ "${_customized_label_color}" = false ]] && _labelcolor="31" [[ "${_customized_label}" = false ]] && _msg_label="Error" shift 1 ;; "") echo "Please specify the message type" >&2 exit 1 ;; *) echo "Unknown message type" >&2 exit 1 ;; esac _word_count="${#_msg_label}" _message="${@}" echo_type() { local __time if [[ "${_nolabel}" = false ]]; then if [[ "${_noadjust}" = false ]]; then for __time in $( seq 1 $(( ${_label_space} - ${_word_count})) ); do echo -ne "${_adjust_chr}" done fi if [[ "${_nocolor}" = false ]]; then echo -ne "\e[$([[ -v _backcolor ]] && echo -n "${_backcolor}"; [[ -v _labelcolor ]] && echo -n ";${_labelcolor}"; [[ -v _decotypes ]] && echo -n ";${_decotypes}")m${_msg_label}\e[m " else echo -ne "${_msg_label} " fi fi } echo_appname() { if [[ "${_noappname}" = false ]]; then if [[ "${_nocolor}" = false ]]; then echo -ne "\e[36m[${_appname}]\e[m " else echo -ne "[${_appname}] " fi fi } for _count in $(seq "1" "$(echo -ne "${_message}\n" | wc -l)"); do _echo_message=$(echo -ne "${_message}\n" |head -n "${_count}" | tail -n 1 ) _full_message="$(echo_appname)$(echo_type)${_echo_message}" case "${_output}" in "stdout") echo ${_echo_opts} "${_full_message}" >&1 ;; "stderr") echo ${_echo_opts} "${_full_message}" >&2 ;; *) echo ${_echo_opts} "${_full_message}" > ${_output} ;; esac done } # Show an INFO message # $1: message string msg_info() { local _msg_opts="-a ${wfa_name}" if [[ "${1}" = "-n" ]]; then _msg_opts="${_msg_opts} -o -n" shift 1 fi [[ "${msgdebug}" = true ]] && _msg_opts="${_msg_opts} -x" [[ "${nocolor}" = true ]] && _msg_opts="${_msg_opts} -n" msg ${_msg_opts} info "${1}" } # Show an Warning message # $1: message string msg_warn() { local _msg_opts="-a ${wfa_name}" if [[ "${1}" = "-n" ]]; then _msg_opts="${_msg_opts} -o -n" shift 1 fi [[ "${msgdebug}" = true ]] && _msg_opts="${_msg_opts} -x" [[ "${nocolor}" = true ]] && _msg_opts="${_msg_opts} -n" msg ${_msg_opts} warn "${1}" } # Show an debug message # $1: message string msg_debug() { if [[ "${debug}" = true ]]; then local _msg_opts="-a ${wfa_name}" if [[ "${1}" = "-n" ]]; then _msg_opts="${_msg_opts} -o -n" shift 1 fi [[ "${msgdebug}" = true ]] && _msg_opts="${_msg_opts} -x" [[ "${nocolor}" = true ]] && _msg_opts="${_msg_opts} -n" msg ${_msg_opts} debug "${1}" fi } # Show an ERROR message then exit with status # $1: message string # $2: exit code number (with 0 does not exit) msg_error() { local _msg_opts="-a ${wfa_name}" if [[ "${1}" = "-n" ]]; then _msg_opts="${_msg_opts} -o -n" shift 1 fi [[ "${msgdebug}" = true ]] && _msg_opts="${_msg_opts} -x" [[ "${nocolor}" = true ]] && _msg_opts="${_msg_opts} -n" msg ${_msg_opts} error "${1}" if [[ -n "${2:-}" ]]; then exit ${2} fi } # rm helper # Delete the file if it exists. # For directories, rm -rf is used. # If the file does not exist, skip it. # remove ... remove() { local _list=($(echo "$@")) _file for _file in "${_list[@]}"; do msg_debug "Removing ${_file}" if [[ -f "${_file}" ]]; then rm -f "${_file}" elif [[ -d "${_file}" ]]; then rm -rf "${_file}" fi done } usage (){ local _pacman_help=false local _wfa_usage _wfa_usage() { echo "Usage:" echo "${wfa_command}" echo "${wfa_command} [...]" echo echo "operations:" echo " ${wfa_command} {-h --help}" echo " ${wfa_command} {-V --version}" #echo " ${wfa_command} {-D --database} " #echo " ${wfa_command} {-F --files} [options] [package(s)]" echo " ${wfa_command} {-Q --query} [options] [package(s)]" echo " ${wfa_command} {-R --remove} [options] " echo " ${wfa_command} {-S --sync} [options] [package(s)]" #echo " ${wfa_command} {-T --deptest} [options] [package(s)]" #echo " ${wfa_command} {-U --upgrade} [options] " #echo #echo "New operations:" #echo " ${wfa_command} {-P --show} [options]" #echo " ${wfa_command} {-G --getpkgbuild} [package(s)]" echo echo "New options:" echo " --repo Assume targets are from the repositories" echo " -a --aur Assume targets are from the AUR" echo echo "Permanent configuration options:" echo " --aururl Set an alternative AUR URL" echo " --makepkg makepkg command to use" echo " --mflags Pass arguments to makepkg" echo " --pacman pacman command to use" echo " --git git command to use" echo " --gitflags Pass arguments to git" echo " --gpg gpg command to use" echo " --gpgflags Pass arguments to gpg" echo " --config pacman.conf file to use" echo " --makepkgconf makepkg.conf file to use" echo " --nomakepkgconf Use the default makepkg.conf" echo } local _wfa_usage_sync _wfa_usage_sync() { echo "usage: ${wfa_command} {-S --sync} [options] [package(s)]" echo "options:" echo " -b, --dbpath set an alternate database location" echo " --config set an alternate configuration file" echo " --noconfirm do not ask for any confirmation" } if [[ "${operation}" = "none" ]]; then _wfa_usage elif [[ "${_pacman_help}" = true ]]; then "${pacman_command}" -h --${operation} elif [[ "$(type -t "_wfa_usage_${operation}" )" = "function" ]]; then _wfa_usage_${operation} else msg_error "Undefined operation." 1 fi } set_operation() { if [[ "${operation}" = "none" ]]; then operation="${1}" add_args pacman "--${operation}" else msg_error "only one operation may be used at a time" 1 fi } run_sudo() { if (( ${UID} == 0 )); then ${@} else sudo ${@} fi } run_pacman() { run_sudo "${pacman_command}" ${@} } # pacmanの引数を追加する # https://github.com/FascodeNet/aptpac/blob/master/aptpac のADD_OPTION関数を参考 # Usage: add_args [pacman/makepkg] ... add_args() { local _target="${1}" local _args_array shift 1 case "${_target}" in "makepkg") _args_array=(${makepkg_args}) _args_array+=(${@}) makepkg_args=${_args_array[@]} msg_debug "MAKEPKG ARGS: ${makepkg_args}" ;; "pacman") _args_array=(${pacman_args}) _args_array+=(${@}) pacman_args=${_args_array[@]} msg_debug "PACMAN ARGS: ${pacman_args}" ;; esac } # 引数で指定されたパッケージがAUR以外の場所に存在しない場合にのみ正常終了します(AURのパッケージの場合に正常終了) check_aur_package() { local _package="${1}" # 参考: https://qiita.com/Hayao0819/items/a8740a17301fafa2fdab if [[ -z "$(pacman -Ssq "${_package}" 2>/dev/null | grep -o ".*${_package}$")" ]]; then #AUR以外のリポジトリに存在しない return 0 else return 1 fi } # 引数で指定されたパッケージが既にインストールされている場合は正常終了します。 check_installed_package() { local _package="${1}" if "${pacman_command}" -Qq "${_package}" > /dev/null 2>&1; then return 0 else return 1 fi } get_cache_dir() { local _user_config_dir if [[ -v XDG_CONFIG_HOME ]]; then _user_config_dir="${XDG_CONFIG_HOME}" else _use_config_dir="${HOME}/.config" fi if [[ -f "${_use_config_dir}/user-dirs.dirs" ]]; then source "${_use_config_dir}/user-dirs.dirs" fi if [[ -v XDG_CACHE_HOME ]]; then echo -n "${XDG_CACHE_HOME}" return 0 else echo -n "${HOME}/.cache" return 0 fi } # Usage: get_srcinfo_data # 参考: https://qiita.com/withelmo/items/b0e1ffba639dd3ae18c0 get_srcinfo_data() { local _srcinfo="${1}" _ver="${2}" local _srcinfo_json=$(python << EOF from srcinfo.parse import parse_srcinfo; import json text = """ $(cat ${1}) """ parsed, errors = parse_srcinfo(text) print(json.dumps(parsed)) EOF ) echo "${_srcinfo_json}" | jq -rc "${2}" | tr '\n' ' ' } # AURからパッケージをビルドしてインストールします # 現在1つのパッケージしか指定できません install_aur_package() { local _package="${1}" # Create cache dir if [[ ! -v wfa_cache_dir ]]; then wfa_cache_dir="$(get_cache_dir)/wfa" fi mkdir -p "${wfa_cache_dir}/archive" mkdir -p "${wfa_cache_dir}/build/${_package}" # AurJsonから値を取得 local _aur_json=$(curl -sL "https://aur.archlinux.org/rpc/?v=5&type=search&by=name&arg=${_package}" | jq -r) if (( "$(echo "${_aur_json}" | jq -r ".resultcount")" == 0 )); then msg_error "Could not find all required packages:\n ${_package}" 1 fi local _found_packages="$(echo "${_aur_json}" | jq -r --tab '.results[].Name')" #msg_debug "Found package: $(echo ${_found_packages} | tr '\n' ' ')" if [[ -n "$(echo "${_found_packages}" | grep -x "${_package}" )" ]]; then msg_debug "Select a package ${_package} with an exact name match" else msg_error "No package with an exact name match was found." 1 fi # PKGBUILDをダウンロード msg_info "Download PKGBUILD of ${_package}" _aur_json=$(echo "${_aur_json}" | jq -r ".results[] | select(.Name == \"${_package}\")" ) local _aur_snapshot_url="${aururl%/}$(echo "${_aur_json}" | jq -r ".URLPath")" local _aur_version="$(echo "${_aur_json}" | jq -r ".Version")" msg_debug "Get PKGBUILD from ${_aur_snapshot_url}" local _pkgbuild_archive_path="${wfa_cache_dir}/archive/${_package}-${_aur_version}" local _download_pkgbuild=true if [[ -f "${_pkgbuild_archive_path}" ]]; then msg_warn "PKGBUILD has already been downloaded." msg_warn -n "Do you want to overwrite and download? [n] :" local _yes_or_no if [[ "${noconfirm}" = true ]]; then echo _yes_or_no="No" else read _yes_or_no fi case "${_yes_or_no}" in "y" | "Y" | "yes" | "Yes" | "YES" ) _download_pkgbuild=true ;; * ) _download_pkgbuild=false ;; esac fi if [[ "${_download_pkgbuild}" = true ]]; then remove "${_pkgbuild_archive_path}" curl -L -C - -f -o "${_pkgbuild_archive_path}" "${_aur_snapshot_url}" fi # PKGBUILDを展開 msg_info "Unpacking the tarball of PKGBUILD ..." tar -xv -f "${_pkgbuild_archive_path}" -C "${wfa_cache_dir}/build/" > /dev/null 2>&1 # .SRCINFOを解析 local _build_dir="${wfa_cache_dir}/build/${_package}" if [[ ! -f "${_build_dir}/.SRCINFO" ]]; then msg_warn ".SRCINFO was not found.\nGenerate it using makepkg." ( cd "${_build_dir}" "${makepkg_command}" --printsrcinfo > "${_build_dir}/.SRCINFO" ) fi local _makedepends="$(get_srcinfo_data "${_build_dir}/.SRCINFO" ".makedepends[]?")" local _depends="$(get_srcinfo_data "${_build_dir}/.SRCINFO" ".depends[]?")" local _conflicts="$(get_srcinfo_data "${_build_dir}/.SRCINFO" ".conflicts[]?")" msg_debug "makedepends: ${_makedepends}" msg_debug "depends: ${_depends}" msg_debug "conflicts: ${_conflicts}" # 衝突を確認 local _pkg _conflicts_found=false for _pkg in ${_conflicts[@]}; do if "${pacman_command}" -Qq "${_pkg}" > /dev/null 2>&1 ; then _conflicts_found=true msg_error "${_package} is colliding with ${_pkg}" fi done if "${pacman_command}" -Qq "${_package}" > /dev/null 2>&1 && [[ ! "$("${pacman_command}" -Qq "${_package}" 2> /dev/null)" = "${_package}" ]]; then msg_error "${_package} is colliding with $("${pacman_command}" -Qq "${_package}")" _conflicts_found=true fi if [[ "${_conflicts_found}" = true ]]; then msg_error "A conflict was found." 1 fi # 依存パッケージをインストール if [[ "${nodeps}" = false ]]; then msg_info "Install dependent packages..." local _force_aur="${force_aur}" force_aur=false install_package "${_depends}" force_aur="${_force_aur}" unset _force_aur fi # ビルド準備 # srcdirの確認 if [[ -d "${_build_dir}/src" ]]; then msg_info "Found ${_build_dir}/src" msg_info "Packages to cleanBuild? [n] :" local _yes_or_no unset _yes_or_no if [[ "${noconfirm}" = true ]]; then echo _yes_or_no="No" else read _yes_or_no fi case "${_yes_or_no}" in "y" | "Y" | "yes" | "Yes" | "YES" ) add_args makepkg "--clean" ;; esac fi # ビルド add_args "${makepkg_command}" "-sf" ( cd "${_build_dir}" "${makepkg_command}" "${makepkg_args}" ) # ビルド後のパッケージ一覧を生成 ( cd "${_build_dir}" makepkg --printsrcinfo > "${_build_dir}/.SRCINFO" ) local _pkgnames=($(get_srcinfo_data "${_build_dir}/.SRCINFO" ".packages | keys[]" | sed 's/ //g')) local _pkgver="$(get_srcinfo_data "${_build_dir}/.SRCINFO" ".pkgver" | sed 's/ //g')" local _pkgrel="$(get_srcinfo_data "${_build_dir}/.SRCINFO" ".pkgrel" | sed 's/ //g')" local _arch_array=($(get_srcinfo_data "${_build_dir}/.SRCINFO" ".arch[]")) local _arch _pkgname if [[ "${_arch_array[*]}" = "any" ]]; then _arch="any" else _arch="$(uname -m)" fi local _PKGEXT=$( source "${makepkg_config}" echo "${PKGEXT}" ) local _pkgfilelist=() for _pkgname in ${_pkgnames[@]}; do _pkgfilelist+=("${_build_dir}/${_pkgname}-${_pkgver}-${_pkgrel}-${_arch}${_PKGEXT}") done # インストール run_pacman -U --noconfirm ${_pkgfilelist[@]} } # バージョンを表示して終了 operation_version() { # Pyalpmからlibalpmの値を取得 # 参考: https://pyalpm.readthedocs.io/en/latest/pyalpm/pyalpm.html local _libalpm_version="$(python3 -c 'import pyalpm; print(pyalpm.alpmversion())')" local _pacman_version="$("${pacman_command}" -Q pacman | cut -d ' ' -f 2)" echo "wfa v${wfa_version} - pacman v${_pacman_version} - libalpm v${_libalpm_version}" } operation_remove() { run_pacman ${pacman_args} "${specified_packages[@]}" } # Usage: install_package ... install_package() { local _package for _package in ${@}; do if ! check_installed_package "${_package}"; then if ! check_aur_package "${_package}"; then # 公式パッケージなのでpacmanでそのままインストール run_pacman ${pacman_args} "${_package}" else # AUR上のパッケージの場合の処理 install_aur_package "${_package}" #msg_error "Getting the AUR package has not been implemented yet." 1 fi fi done } operation_sync(){ local _package for _package in ${specified_packages[@]}; do if ! check_aur_package "${_package}" && [[ "${force_aur}" = false ]]; then # 公式パッケージなのでpacmanでそのままインストール run_pacman ${pacman_args} "${_package}" else # AUR上のパッケージの場合の処理 install_aur_package "${_package}" #msg_error "Getting the AUR package has not been implemented yet." 1 fi done } # Parse options ARGUMENT="${@}" _opt_short="QRShVdb:a" _opt_long="query,remove,sync,help,version,debug,dbpath:,aururl,aur,noconfirm,config:,makepkg:,mflags:,pacman:,git:,gitflags:,gpg:,gpgflags:,makepkgconf:,nomakepkgconf,nodeps" OPT=$(getopt -o ${_opt_short} -l ${_opt_long} -- ${ARGUMENT}) [[ ${?} != 0 ]] && exit 1 unset _opt_short _opt_long eval set -- "${OPT}" msg_debug "Argument: ${OPT}" while :; do case ${1} in -Q | --query) set_operation "query" shift 1 ;; -R | --remove) set_operation "remove" shift 1 ;; -S | --sync) set_operation "sync" shift 1 ;; -V | --version) set_operation "version" shift 1 ;; --) shift break ;; *) shift 1 ;; esac done eval set -- "${OPT}" while :; do case ${1} in -a | --aur) force_aur=true msg_debug "Assume targets are from the AUR" shift 1 ;; --debug) debug=true add_args pacman "--debug" shift 1 ;; -d | --nodeps) nodeps=true add_args pacman "--nodeps" shift 1 ;; -b | --dbpath) add_args pacman "--dbpath '${2}'" shift 2 ;; --aururl) aururl="${2}" shift 2 ;; --noconfirm) add_args pacman "--noconfirm" noconfirm=true shift 1 ;; --config) pacman_config="${2}" add_args pacman "--config \"${2}\"" shift 2 ;; --makepkg) makepkg_command="${2}" shift 2 ;; --mflags) makepkg_args="${2}" shift 2 ;; --pacman) pacman_command="${2}" shift 2 ;; --git) git_command="${2}" shift 2 ;; --gitflags) git_args="${2}" shift 2 ;; --makepkgconfig) if [[ "${nomakepkgconf}" = false ]]; then makepkg_config="${2}" else msg_warn "--nomakepkgconf is specified.\n--makepkgconf has been ignored." shift 2 ;; --nomakepkgconf) makepkg_config="/etc/makepkg.conf" nomakepkgconf=true shift 1 ;; -h | --help) usage shift 1 exit 0 ;; --) shift break ;; *) shift 1 ;; esac done specified_packages=(${@}) case "${operation}" in "version") operation_version ;; "sync") operation_sync ;; "remove") operation_remove ;; "none") exit 0 ;; *) msg_error "Undefined operation." 1 ;; esac