#! /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