#! /bin/bash
# This program is part of the Qi package manager
#
# Copyright (C) 2015, 2016 Matias A. Fonzo
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# EXIT STATUS
# 0 = Successful completion
# 1 = Minor common errors (e.g: help usage, support not available)
# 2 = Command execution error
# 3 = Integrity check error for compressed files
# 4 = File empty, not regular, or expected
# 5 = Empty or not defined variable
# 10 = Network manager error
# Override locale settings
export LC_ALL=C
# Functions
show_help() {
printf '%s\n' \
"Usage: qi build [options] recipe(s)" \
"Build packages using recipe files." \
"" \
"When recipe is -, read standard input." \
"" \
"Build command options:" \
" -h display this help and exit" \
" -A avoid automatic error detection" \
" -C create named pipe for communicate package name" \
" -N don't read runtime configuration file" \
" -S don't strip unneeded or debugging symbols from files" \
" -k keep (don't delete) srcdir and destdir" \
" -n don't create a .tlz package" \
" -o where the produced packages are written" \
" -t temporary directory for compilation" \
" -w working directory tree for archive, patches, recipes" \
" -z where to find the compressed sources" \
" -a architecture to use" \
" -i increment release number when a package is produced" \
" -j parallel jobs for the compiler" \
""
}
# Directory of functions
readonly fndir="${LIBEXECDIR}/functions"
# Load (externally) internal functions
source "${fndir}"/helpers || exit 1
source "${fndir}"/readconfig || exit 1
unpack() {
local file tool_verify tool_extract
for file in "${tardir}/${@##*/}" ; do
if [[ ! -f $file ]] ; then
quit 2 "qi: build: Cannot unpack '${file}': File not found or not regular."
fi
# Assign an appropriate tool for options according to the file extension
case "$file" in
*.tar.*|*.tar)
tool_verify="tar -tf"
tool_extract="tar -xf"
;;
*.zip|*.ZIP)
tool_verify="unzip -t"
tool_extract=unzip
;;
*)
quit 1 "qi: build: ${file}: Unsupported file format, extension '${file#*.}'.";
esac
echo "==> Checking SHA1 sums ..."
if [[ -f ${file}.sha1 ]] ; then
eval_cmd "( cd "$tardir" && sha1sum -c "${file}.sha1" )"
fi
echo "==> Checking integrity of '${file}' ..."
eval_cmd 3 "$tool_verify "$file" > /dev/null"
# Generate a check sum or update it according to the timestamp of the source
if [[ ! -e ${file}.sha1 || $file -nt ${file}.sha1 ]] ; then
echo "==> SHA1: Creating or updating '${file}.sha1' ..."
eval_cmd "( cd "$tardir" && sha1sum "${file##*/}" > "${file}.sha1" )"
fi
echo "==> Unpacking source '${file}' ..."
eval_cmd 3 "$tool_extract "$file""
done
}
# Default variable values
avoid_errors=noavoid_errors
create_pipe=nocreate_pipe
rcfile=rcfile
strip=strip
keepdir=nokeepdir
nopkg=no_nopkg
outdir=/var/cache/qi/packages
tmpdir=/tmp/sources
worktree=/usr/src/qi
tardir=${worktree}/sources
arch="$(uname -m)"
increment_release=noincrement_release
jobs=1
netget="wget --continue --wait=1 --tries=3 --no-check-certificate --passive-ftp"
rsync="rsync --verbose --archive --copy-links --compress --progress --itemize-changes"
compress_pages=compress_pages
# Handle command-line options.
#
# An indexed array is created when an option is set. It helps to the
# command line in order to take precedence over the config file
while getopts ":hACNSkni o:t:w:z:a:j:" option ; do
case "$option" in
(k)
keepdir=keepdir
;;
(A)
avoid_errors=avoid_errors
;;
(C)
create_pipe=create_pipe
;;
(N)
rcfile=no_rcfile
;;
(S)
strip=nostrip
cmdLineSets+=( strip )
;;
(n)
nopkg=nopkg
;;
(o)
outdir="$OPTARG"
cmdLineSets+=( outdir )
;;
(t)
tmpdir="$OPTARG"
cmdLineSets+=( tmpdir )
;;
(w)
worktree="$OPTARG"
cmdLineSets+=( worktree )
;;
(z)
tardir="$OPTARG"
cmdLineSets+=( tardir )
;;
(a)
arch="$OPTARG"
cmdLineSets+=( arch )
;;
(i)
increment_release=increment_release
;;
(j)
jobs="$OPTARG"
cmdLineSets+=( jobs )
;;
(h)
show_help
exit 0
;;
:)
quit 1 \
"The option '-${OPTARG}' requires an argument" \
"Usage: qi build [-hACNSkni] [-otwz
] [-a ] [-j ] [recipe ...]"
;;
\?)
quit 1 \
"qi: build: illegal option -- '-${OPTARG}'" \
"Usage: qi build [-hACNSkni] [-otwz ] [-a ] [-j ] [recipe ...]"
;;
esac
done
shift "$((OPTIND - 1))"
# If there are no arguments, one recipe is required
if (( $# == 0 )) ; then
quit 4 \
"\nqi: build: You must specify at least one recipe file." \
"Try 'qi build -h' for more information."
fi
# To mark some variables. Redefinitions of this variables
# are not allowed from the config, or from the recipes
readonly create_pipe nopkg keepdir increment_release
# Read configuration file
if [[ $rcfile = rcfile ]] ; then
# Check existence, from where to read
if [[ -e ~/.qirc ]] ; then
configFrom="~/.qirc" # $HOME.
elif [[ -e @SYSCONFDIR@/qirc ]] ; then
configFrom="@SYSCONFDIR@/qirc" # System-wide.
fi
# Check file integrity, content
if [[ -f $configFrom && -s $configFrom ]] ; then
echo "Processing '${configFrom}' ..."
readConfig < "$configFrom"
else
warn "WARNING: ${configFrom}: File empty or not regular."
fi
fi
# Unset from memory unnecessary variables, arrays, functions
unset rcfile configFrom cmdLineSets readConfig
# Set default mask
umask 022
# Create required directories (if needed)
eval_cmd "mkdir -p "${worktree}"/{archive,patches,recipes} "$tardir""
# Read from the standard input if '-' was given
if [[ $1 = - ]] ; then
set -- # Unset positional parameters, setting '#' to zero.
while IFS="" read -r filename ; do
set -- "$@" "$filename"
done
fi
# We prepare to export specific variables that will serve to restore
# their values when one stops processing a recipe and place is given
# to the following one
export avoid_errors arch jobs strip
# Create a Private Randomized Directory
PRD="qi-build-${USER}-$RANDOM$$"
eval_cmd "mkdir -p -m 700 "${tmpdir}"/$PRD"
# To remove directory and pipe when it leaves
trap 'eval_cmd "rm -rf "${tmpdir}"/$PRD" /var/tmp/qi-pipe' EXIT
# Save all exported variables
export -p > "${tmpdir}"/${PRD}/default_env$$
# Mark some variables as read only.
#
# This allows the use without modification on the recipes
readonly outdir tmpdir worktree tardir netget rsync PRD
# Main loop
for recipe ; do
if [[ ! -f $recipe ]] ; then
quit 4 "qi: build: File '${recipe}' not found or not regular."
fi
# Save the current working directory. We will go back here later.
CWD="$PWD"
echo "==> Loading \"${recipe}\" from \`${CWD}' ..."
# Mark all the variables for export
set -o allexport
# Include recipe
source "$recipe"
# Sanity check: checking required variables
if [[ -z $program ]] ; then
quit 5 "qi: build: 'program' is not defined."
fi
if [[ -z $version ]] ; then
quit 5 "qi: build: 'version' is not defined."
fi
if [[ -z $release ]] ; then
quit 5 "qi: build: 'release' is not defined."
fi
if [[ $increment_release = increment_release ]] ; then
release="$((release + 1))"
fi
if [[ -z $tardir ]] ; then
quit 5 "qi: build: 'tardir' is not defined."
fi
# Allow the dot as the current working directory
if [[ $tardir = . ]] ; then
tardir="$CWD"
fi
if [[ -z $tarname ]] ; then
quit 5 "qi: build: 'tarname' is not defined."
fi
shopt -s extglob
if [[ $jobs != +([0-9]) ]] ; then
quit 5 "qi: build: '$jobs' needs to be a valid number."
fi
shopt -u extglob # Deactivate shell option.
# Check if build() is present
if [[ $(command -v build) != build ]] ; then
quit 1 "qi: build: The main function build() is not present."
fi
# Use or assign default values for variables
srcdir="${srcdir:-$program-$version}"
destdir="${destdir:-${tmpdir}/package-$program}"
arch="${arch:=$(uname -m)}"
case "$arch" in
x86_64)
export libSuffix=64
;;
esac
infodir="${infodir:-/usr/share/info}"
mandir="${mandir:-/usr/share/man}"
docdir="${docdir:-/usr/share/doc/${program}-${version}}"
pkgname="${pkgname:-$program}"
full_pkgname="${pkgname}-${version}-${arch}+${release}"
full_tarname="${tardir}/$tarname"
echo "==> Preparing temporary directories ..."
rmIfDir "${tmpdir}/${srcdir}" "$destdir"
echo "==> Creating required directories for the package ..."
eval_cmd "mkdir -p "$outdir" "${destdir}"/var/lib/qi/recipes"
# Fetch remote sources
echo "==> Fetching remote source(s) if they do not exist locally ..."
if (( ${#fetch[@]} > 0 )) ; then
for address in "${fetch[@]}" ; do
file_name="${address##*/}"
if [[ ! -r ${tardir}/$file_name ]] ; then
warn "Source file '${file_name}' not found in \`${tardir}'."
echo "==> Obtaining source from ${address%/*} ..."
case "$address" in
rsync://*)
eval_cmd "cd "$tardir""
eval_cmd 10 ""${rsync//[\"\']/}" "$address""
;;
*://*)
eval_cmd "cd "$tardir""
eval_cmd 10 ""${netget//[\"\']/}" "$address""
;;
*)
quit 1 "qi: build: Source file '${file_name}' cannot be obtained, protocol not supported.";
esac
# Update timestamp and SHA1 sum
eval_cmd \
"touch "$file_name" && sha1sum "$file_name" > "${file_name}.sha1sum""
fi
done
unset address file_name fetch
else
warn "qi: build: WARNING: Array 'fetch' empty. No source(s) will be downloaded."
fi
# Decompress main source in tmpdir
eval_cmd "cd "$tmpdir" && unpack "$full_tarname""
# Set sane ownerships
if (( $UID == 0 )) ; then
eval_cmd "chown -R 0:0 "${tmpdir}/${srcdir}""
fi
# Set sane permissions
eval_cmd "chmod -R u+w,go-w,a+rX-s "${tmpdir}/${srcdir}""
# Exit immediately on any error before build()
if [[ $avoid_errors != avoid_errors ]] ; then
set -o errexit # Exit immediately on any error.
else
warn "WARNING: Automatic detection of errors will be avoided."
fi
# Run the build function
echo "==> Running build() ..."
build
set +o errexit # Deactivate shell option.
# If for some reason destdir is empty, the package won't be created
if rmdir "$destdir" 2> /dev/null ; then
warn "qi: build: WARNING: ${full_pkgname}.tlz: Won't be created, 'destdir' is empty."
nopkg=nopkg
strip=nostrip
fi
# Strip binaries and libraries
if [[ $strip != nostrip && $arch != noarch ]] ; then
echo "==> Stripping binaries and libraries ..."
shopt -s globstar # To find all the files recursively
for element in "${destdir}"/** ; do
if [[ ! -e $element ]] ; then
continue;
fi
if [[ -f $element && -s $element ]] ; then
fileList+=( $element )
fi
done
# Iterate over the fileList
if (( ${#fileList[@]} > 0 )) ; then
for element in "${fileList[@]}" ; do
string="$(file -- "$element")"
if [[ $string =~ ELF && $string =~ executable|'shared object'|relocatable ]] ; then
strip --strip-unneeded "$element"
fi
if [[ $string =~ 'current ar archive' ]] ; then
strip --strip-debug "$element"
fi
done
else
warn "qi: build: WARNING: No elements or symbols found to strip."
fi
shopt -u globstar # Deactivate shell option.
unset element string fileList
fi
# Copy the documentation
if [[ $nopkg != nopkg ]] ; then
echo "==> Copying documentation ..."
if [[ $compress_pages != nocompress_pages ]] ; then
# Compress .info documents (if needed)
if [[ -d ${destdir}/$infodir ]] ; then
echo "--- Info documents:"
rm -f "${destdir}/${infodir}"/dir
while IFS="" read -r ; do
echo "${REPLY##*/}" # Show processed file
eval_cmd "lzip -9 "$REPLY"" # Compress it using the max level
done < <(eval_cmd "find "${destdir}/${infodir}" -type f")
unset REPLY
fi
# Compress and link manual pages (if needed)
if [[ -d ${destdir}/$mandir ]] ; then
echo "--- Manual pages:"
eval_cmd \
"( cd "${destdir}/$mandir" && find . -type f -exec lzip -9 '{}' \; -print)"
# Make soft links
while IFS="" read -r ; do
eval_cmd "ln -sf -v "$(readlink -- "$REPLY").lz" "${REPLY}.lz""
eval_cmd "rm -v -- "$REPLY""
done < <(eval_cmd "find "${destdir}/$mandir" -type l")
unset REPLY
fi
fi
# Source documentation
if (( ${#docs[@]} > 0 )) ; then
echo "--- Source documentation:"
eval "mkdir -p "${destdir}/$docdir""
shopt -s globstar
for element in "${docs[@]}" ; do
eval_cmd "cp -a "$element" "${destdir}/$docdir""
done
shopt -u globstar
unset element docdir
echo "${#docs[@]} elements have been copied from the 'docs' array."
unset docs
fi
fi
# Package creation
if [[ $nopkg != nopkg ]] ; then
echo "==> Creating package \"${full_pkgname}.tlz\" in \`${outdir}' ..."
# Recipe edition when release is incremented
if [[ $increment_release = increment_release ]] ; then
eval_cmd "ed -s "$recipe"" <<< ",s/^\(release\)=.*/\1=${release}/"$'\nw'
fi
# Add recipe to the (package) database
eval_cmd "( cd "$CWD" && cp -p "$recipe" "${destdir}/var/lib/qi/recipes/" )"
# Make the package
eval_cmd \
"cd "$destdir" && tar -c ./* | lzip -9v > "${outdir}/${full_pkgname}.tlz""
fi
# Save variables of the current recipe and possible exported functions
eval_cmd "cd "${tmpdir}"/$PRD && export -p > recipe_env$$"
# Prints the lines only in recipe_env but not in default_env, extracting
# the name of the variable and function for easy deactivation
eval_cmd \
"grep -F -xvf default_env$$ recipe_env$$ | sed '/^declare -x/!d ; s/^declare -x \([^=]*\).*/unset \1/' > substraction$$"
# Deactivate exportation
set +o allexport
# Back to the current working directory
eval_cmd "cd "$CWD""
# Clean temporary directories
if [[ $keepdir != keepdir ]] ; then
echo "==> Cleaning up temporary directories ..."
rmIfDir "${tmpdir}/${srcdir}" "$destdir"
fi
# Save pkgname before restoring the variables,
# this will be used to send the pkgname via pipe
s_full_pkgname="$full_pkgname"
# Deactivate variables used in the recipe
source "${tmpdir}"/${PRD}/substraction$$
# Restore default environment for the next recipe
source "${tmpdir}/${PRD}/default_env$$" 2> /dev/null
echo "<== Done [ ${CWD}/$recipe ]"
# Create named pipe if -C is given (server/writer)
if [[ $create_pipe = create_pipe ]] ; then
if [[ ! -p /var/tmp/qi-pipe ]] ; then
eval_cmd "mkfifo -m 077 /var/tmp/qi-pipe"
fi
printf '%s\n' \
"qi: build: PIPE: Sending full package name to /var/tmp/qi-pipe ..." \
"qi: build: PIPE: Waiting for the reader ..."
eval_cmd "echo \"${outdir}/${s_full_pkgname}.tlz\" > /var/tmp/qi-pipe"
fi
unset s_full_pkgname
done