qi: Bump to version 2.0 (Release Candidate 12)
[dragora.git] / qi / src / qi.in
blob5bd5d425e02e30a9001f290145300a5108c7d8b1
1 #! /bin/sh -
2 # Copyright (C) 2016-2020 Matias Fonzo <selk@dragora.org>
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 # EXIT STATUS
18 # 0 = Successful completion
19 # 1 = Minor common errors (e.g: help usage, support not available)
20 # 2 = Command execution error
21 # 3 = Integrity check error for compressed files
22 # 4 = File empty, not regular, or expected
23 # 5 = Empty or not defined variable
24 # 6 = Package already installed
25 # 10 = Network manager error
27 PROGRAM="${0##*/}"
29 LC_ALL=C; # Override locale settings.
30 umask 022; # Remove write permission for group and other.
31 export LC_ALL
33 ### Functions
35 usage()
37 printf '%s\n' \
38 "Qi is a simple but well-integrated package manager. It can create," \
39 "install, remove, and upgrade software packages. Qi produces binary" \
40 "packages using recipes, which are files containing specific instructions" \
41 "to build each package from source. Qi can manage multiple packages" \
42 "under a single directory hierarchy. This method allows to maintain a" \
43 "set of packages and multiple versions of them. This means that Qi could" \
44 "be used as the main package manager or complement the existing one." \
45 "" \
46 "Usage: $PROGRAM COMMAND [OPTIONS] [FILE]..." \
47 "" \
48 "Defaults for the options are specified in brackets." \
49 "" \
50 "List of commands:" \
51 " warn Warn about files that will be installed" \
52 " install Install packages" \
53 " remove Remove packages" \
54 " upgrade Upgrade packages" \
55 " extract Extract packages for debugging purposes" \
56 " create Create a .tlz package from directory" \
57 " build Build packages using recipe names" \
58 " order Resolve build order through .order files" \
59 "" \
60 "Options when installing, removing, or upgrading software packages:" \
61 " -f, --force Force upgrade of pre-existing packages" \
62 " -k, --keep Keep package directory when remove/upgrade" \
63 " -p, --prune Prune conflicts" \
64 " -P, --packagedir=<dir> Set directory for package installations" \
65 " -t, --targetdir=<dir> Set target directory for symbolic links" \
66 " -r, --rootdir=<dir> Use the fully qualified named directory as" \
67 " the root directory for all qi operations" \
68 " Note: the target directory and the package" \
69 " directory will be relative to the specified" \
70 " directory, excepting the graft log file" \
71 "" \
72 "Options when building software packages using recipes:" \
73 " -a, --architecture Set architecture name for the package [${arch}]" \
74 " -j, --jobs Parallel jobs for the compiler [${jobs}]" \
75 " -k, --keep Keep \`\${srcdir}' or \`\${destdir}' when build" \
76 " -S, --skip-questions Skip questions on completed recipes" \
77 " -1, --increment Increment release number (\`\${release}' + 1)" \
78 " -n, --no-package Do not create a .tlz package" \
79 " -i, --install Install package after the build" \
80 " -u, --upgrade Upgrade package after the build" \
81 " -o, --outdir=<dir> Where the packages produced will be written [${outdir}]" \
82 " -w, --worktree=<dir> Where archives, patches, recipes are expected [${worktree}]" \
83 " -s, --sourcedir=<dir> Where compressed sources will be found [${tardir}]" \
84 "" \
85 "Other options:" \
86 " -N, --no-rc Do not read the configuration file" \
87 " -v, --verbose Be verbose (an extra -v gives more) [${verbose}]" \
88 " -L, --show-location Print default directory locations and exit" \
89 " -h, --help Display this help and exit" \
90 " -V, --version Output version information and exit" \
91 "" \
92 "Some influential environment variables:" \
93 " TMPDIR Temporary directory for sources [${TMPDIR}]" \
94 " QICFLAGS C compiler flags [${QICFLAGS}]" \
95 " QICXXFLAGS C++ compiler flags [${QICXXFLAGS}]" \
96 " QILDFLAGS Linker flags [${QILDFLAGS}]" \
97 "" \
98 "When FILE is -, read standard input." \
99 "" \
100 "Exit status: 0 for a normal exit, 1 for minor common errors (help usage," \
101 "support not available, etc), 2 to indicate a command execution error;" \
102 "3 for integrity check error on compressed files, 4 for empty, not" \
103 "regular, or expected files, 5 for empty or not defined variables," \
104 "6 when a package already exist, 10 for network manager errors." \
105 "" \
106 "Qi home page: https://www.dragora.org"
109 warn()
111 printf '%s\n' "$@" 1>&2
114 is_readable()
116 if test -e "$1"
117 then
118 if ! test -r "$1"
119 then
120 echo "${PROGRAM}: cannot read ${1}: Permission denied" 1>&2
121 return 1
123 else
124 echo "${PROGRAM}: cannot access ${1}: No such file or directory" 1>&2
125 return 1
129 # Portable alternative to the file operator -nt (among shells)
130 is_newer()
132 if test -n "$(find "$1" -prune -newer "$2" -print)"
133 then
134 return 0
136 return 1
139 # Determine whether $2 matches pattern $1
140 fnmatch()
142 case $2 in
144 return 0
147 return 1
149 esac
152 # A shell replacement for basename(1)
153 _basename()
155 _filename="${1##*/}"
157 printf '%s\n' "${_filename%$2}"
159 unset _filename
162 chkstatus_or_exit()
164 status=$?
166 if test $status -ne 0
167 then
168 echo "^ Return status = $status" 1>&2
169 exit "${1-2}"; # If not given, defaults to 2
172 unset status
175 readconfig()
177 if test "$readconfig" = readconfig
178 then
179 is_readable "$HOME/.qirc" 2> /dev/null && _rcfile="$HOME/.qirc";
181 echo "Importing configuration file \`${_rcfile}' ..."
183 test -f "$_rcfile" || {
184 warn "$_rcfile is not a regular file."
185 return 1;
188 . "$_rcfile"
192 ### Mode functions
194 mode_build()
196 echo ""
197 echo "{@} Building \"${1}\" ..."
199 recipe=$1
201 # A recipe is any valid regular file. Qi sets priorities for reading a
202 # recipe, the order in which qi looks for a recipe is:
204 # 1. Current working directory.
206 # 2. If the specified path name does not contain "recipe" as the last
207 # component. Qi will complete it by adding "recipe" to the path
208 # name.
210 # 3. If the recipe is not in the current working directory, it will be
211 # searched under '${worktree}/recipes'. The last component will be
212 # completed adding "recipe" to the specified path name.
214 if test ! -f "$recipe"
215 then
216 if test -f "${recipe}/recipe"
217 then
218 recipe="${recipe}/recipe"
219 elif test -f "${worktree}/recipes/${recipe}/recipe"
220 then
221 recipe="${worktree}/recipes/${recipe}/recipe"
225 test -f "$recipe" || {
226 warn "\`${recipe}' is not a regular file."
227 return 4
230 # Complain if the file name is not "recipe"
232 if test "${recipe##*/}" != recipe
233 then
234 warn "\`${recipe}' is not a valid recipe name."
235 return 4
238 # Start preparations to import the recipe
240 # Get working directory and base name of the recipe
242 CWD="$(dirname -- "$recipe")"
243 recipe="$(_basename "$recipe")"
245 # Check readability for load the recipe on success
247 is_readable "${CWD}/$recipe" || exit 4
249 # Re-create external directories
250 mkdir -p -- "${worktree}/archive" \
251 "${worktree}/patches" \
252 "${worktree}/recipes" \
253 "$tardir"
255 # Variables treatment for the current and the next recipe.
257 # Unset special variables that can only be predefined in
258 # the recipe and does not come from '${sysconfdir}/qirc'
260 unset srcdir destdir pkgname pkgversion pkgcategory program version release \
261 fetch description homepage license replace full_pkgname \
262 CFLAGS CXXFLAGS LDFLAGS
264 # The following variables must be restored, later
265 save_arch="${save_arch:=$arch}"
266 save_jobs="${save_jobs:=$jobs}"
267 save_outdir="${save_outdir:=$outdir}"
268 save_opt_nopkg="${save_opt_nopkg:=$opt_nopkg}"
270 # Reset variable values in case of return
271 arch="$save_arch"
272 jobs="$save_jobs"
273 outdir="$save_outdir"
274 opt_nopkg="$save_opt_nopkg"
276 # The following variables cannot be redefined on the recipe
277 readonly worktree netget rsync
279 # Import the recipe
281 . "${CWD}/$recipe"
282 chkstatus_or_exit
284 # Check for required variables
285 if test -z "$program"
286 then
287 warn "${recipe}: The variable 'program' is not defined."
288 exit 5
290 if test -z "$version"
291 then
292 warn "${recipe}: The variable 'version' is not defined."
293 exit 5
295 if test -z "$release"
296 then
297 warn "${recipe}: The variable 'release' is not defined."
298 exit 5
301 # Pre-settings before to start building
303 # Increment the release number if the option was given
304 if test "$opt_incr_release" = opt_incr_release
305 then
306 release=$(( release + 1 ))
309 # Allow the dot as definition for 'tardir'
310 if test "$tardir" = .
311 then
312 tardir="$CWD"
315 # Set default values for the following special variables
317 pkgname="${pkgname:=$program}"
318 pkgversion="${pkgversion:=$version}"
319 srcdir="${srcdir:=${program}-$version}"
320 destdir="${destdir:=${TMPDIR}/package-$pkgname}"
322 # If 'pkgcategory' has been defined, prefix it using the "at" symbol
323 if test -n "$pkgcategory"
324 then
325 pkgcategory="@${pkgcategory}"
328 # Compose the full package name
329 full_pkgname="${full_pkgname:=${pkgname}_${pkgversion}_${arch}-${release}${pkgcategory}}"
331 # Use 'arch' as suffix for 'outdir' to have a well-organized package output
332 outdir="${outdir}/${arch}"
334 # If a package is going to be created the existence of a
335 # previous build will be detected and reported. Under normal
336 # conditions the recipe is built as long as it is newer than
337 # the produced package, if not, we warn to the user about it.
338 # Rebuilding the package is possible (through the force ;-)
340 if test "$opt_nopkg" != opt_nopkg && \
341 { test "$opt_force" != opt_force && \
342 test -e "${outdir}/${full_pkgname}.tlz" ; }
343 then
344 if is_newer "${CWD}/$recipe" "${outdir}/${full_pkgname}.tlz"
345 then
346 warn \
347 "" \
348 "The recipe is more RECENT than the detected package:" \
349 "" \
350 "$( stat -c "%y %n" "${CWD}/$recipe" )" \
351 "$( stat -c "%y %n" "${outdir}/${full_pkgname}.tlz" )" \
352 "" \
353 " This recipe will be processed ..." \
355 elif test -e "${CWD}/post-install" && \
356 is_newer "${CWD}/post-install" "${CWD}/$recipe"
357 then
358 warn \
359 "" \
360 "The post-install script is more RECENT than the recipe:" \
361 "" \
362 "$( stat -c "%y %n" "${CWD}/post-install" )" \
363 "$( stat -c "%y %n" "${CWD}/$recipe" )" \
364 "" \
365 " The recipe will be re-processed ..." \
367 touch "${CWD}/$recipe"
368 elif test "$opt_skipqsts" = opt_skipqsts
369 then
370 warn "Recipe for '${full_pkgname}.tlz': Ignored." ""
371 return 0
372 else
373 warn \
374 "" \
375 "This recipe ALREADY produced a package:" \
376 "$( stat -c "%y %n" "${outdir}/${full_pkgname}.tlz" )" \
377 "" \
378 "The recipe is still OLDER than the produced package:" \
379 "$( stat -c "%y %n" "${CWD}/$recipe" )" \
380 "" \
381 " Probably nothing has changed." \
384 # In non-interactive mode, the user is asked about
385 # rebuilding the package. In interactive mode,
386 # the user need to pass the option explicitly
387 if test ! -t 0
388 then
389 printf '%s\n' \
390 "Do you want to rebuild this package?" \
391 "1) Yes, built it" \
392 "2) No, skip it [default]" \
393 "3) Resume, skipping completed recipes" \
394 "Enter an option number:" > /dev/tty
395 IFS= read -r ANSWER < /dev/tty || exit 2;
396 case $ANSWER in
398 echo "$ANSWER" > /dev/tty
399 unset ANSWER
402 unset ANSWER
403 echo "=== Building UNPROCESSED/MODIFIED recipes ..." > /dev/tty
404 echo ""
405 opt_skipqsts=opt_skipqsts
406 readonly opt_skipqsts
407 return 0
410 unset ANSWER
411 echo "Recipe for '${full_pkgname}.tlz': Cancelled." > /dev/tty
412 echo ""
413 return 0
415 esac
416 else
417 warn "Use the --force option to reprocess ${CWD}/$recipe." ""
418 return 6;
423 # Fetch remote sources
425 echo "=== Fetching remote sources if needed ..."
426 if test -n "$fetch"
427 then
428 for origin in $fetch
430 _source="${origin##*/}"
432 echo "=== Looking for \"$_source\" ..."
434 echo "=== Verifying checksum file \`${tardir}/${_source}.sha256'"
435 if test -e "${tardir}/${_source}.sha256"
436 then
437 ( cd -- "$tardir" && sha256sum - ) < "${tardir}/${_source}.sha256"
438 chkstatus_or_exit
439 continue;
440 else
441 warn "${_source}.sha256: Checksum file does not exist, yet."
444 # Download source or resume if allowed
446 if test ! -e "${tardir}/$_source"
447 then
448 warn " Can't find \"${_source}\" at \"${tardir}\";" \
449 "attempting to get it from ${origin%/*} ..."
452 case $origin in
453 rsync://*)
455 cd -- "$tardir" && $rsync "$origin" || exit $?
456 sha256sum "$_source" > "${_source}.sha256"
457 ); chkstatus_or_exit 10
459 *://*)
461 cd -- "$tardir" && $netget "$origin" || exit $?
462 sha256sum "$_source" > "${_source}.sha256"
463 ); chkstatus_or_exit 10
466 warn "${PROGRAM}: Unrecognized protocol for ${origin}."
467 exit 4
468 esac
469 done
470 unset origin _source
471 else
472 warn "${recipe}: The variable 'fetch' is empty."
475 # Prepare special directories for build the source,
476 # the destination and the output of the package
478 echo "=== Preparing directories ..."
480 if test -z "$keep_srcdir"
481 then
482 if test -e "${TMPDIR}/$srcdir"
483 then
484 rm -r -- "${TMPDIR:?}/$srcdir" || chkstatus_or_exit
485 echo "removed directory: '${TMPDIR}/$srcdir'"
487 else
488 warn \
489 "WARNING: The variable 'keep_srcdir' has been set (${keep_srcdir})."
492 if test -z "$keep_destdir"
493 then
494 if test -e "$destdir"
495 then
496 rm -r -- "$destdir" || chkstatus_or_exit
497 echo "removed directory: '$destdir'"
499 mkdir -p -- "$destdir" || chkstatus_or_exit
500 echo "mkdir: created directory '$destdir'"
503 if test ! -e "$outdir"
504 then
505 mkdir -p -- "$outdir" || chkstatus_or_exit
506 echo "mkdir: created directory '$outdir'"
509 echo "=== Changing to '${TMPDIR}' ..."
510 cd -- "$TMPDIR" || chkstatus_or_exit
512 # Set trap before to run the build() function in order
513 # to catch the return status, exit code 2 if fails
515 trap 'chkstatus_or_exit 2' EXIT HUP INT QUIT ABRT TERM
517 # Determine if the debugging indicators of the shell should be
518 # retained, assuming that it has been previously passed
519 case $- in *x*)
520 _xtrace_flag=_xtrace_flag_is_set ;;
521 esac
523 echo "=== Running build() function ..."
524 build
525 unset build
527 # Turn off possible shell flags coming from the recipe
529 set +e
530 if test "${_xtrace_flag:+$_xtrace_flag}" != _xtrace_flag_is_set
531 then
532 set +x
535 # Reset given signals
536 trap - EXIT HUP INT QUIT ABRT TERM
538 # If 'destdir' is empty, the package won't be created
539 if rmdir -- "$destdir" 2> /dev/null
540 then
541 warn "The package \"${full_pkgname}.tlz\" won't be created. 'destdir' is empty."
542 opt_nopkg=opt_nopkg
545 # Create (make) the package
547 if test "$opt_nopkg" != opt_nopkg
548 then
549 # Edit the recipe when 'release' is incremented
550 if test "$opt_incr_release" = opt_incr_release
551 then
552 echo ",s/^\\(release\\)=.*/\\1=${release}/"$'\nw' | \
553 ed "${CWD}/$recipe" || chkstatus_or_exit
556 mkdir -p -- "${destdir}/var/lib/qi" || chkstatus_or_exit
558 # Include a copy of the recipe into the package
559 cp -p "${CWD}/$recipe" \
560 "${destdir}/var/lib/qi/${full_pkgname}.recipe" && \
561 chmod 644 "${destdir}/var/lib/qi/${full_pkgname}.recipe"
562 chkstatus_or_exit
564 # Detect post-install script for inclusion
566 if test -f "${CWD}/post-install"
567 then
568 echo "${CWD}/post-install: Detected."
569 cp -p "${CWD}/post-install" \
570 "${destdir}/var/lib/qi/${full_pkgname}.sh" && \
571 chmod 644 "${destdir}/var/lib/qi/${full_pkgname}.sh"
572 chkstatus_or_exit
575 # Detect declared package name(s) for later replacement
577 if test -n "$replace"
578 then
579 warn \
580 "=== The following package names has been declared for replacement:" \
581 "$replace"
583 rm -f "${destdir}/var/lib/qi/${full_pkgname}.replace"
584 for item in $replace
586 printf '%s\n' "$replace" >> \
587 "${destdir}/var/lib/qi/${full_pkgname}.replace"
588 done
589 unset item
592 # Create (external) meta file for package information,
593 # make a copy of it for the package database
594 echo "=== Creating meta file ${full_pkgname}.tlz.txt ..."
595 do_meta > "${outdir}/${full_pkgname}.tlz.txt" || chkstatus_or_exit
596 cp -p "${outdir}/${full_pkgname}.tlz.txt" \
597 "${destdir}/var/lib/qi/${full_pkgname}.txt" || chkstatus_or_exit
599 # Produce the package
600 cd -- "$destdir" && mode_create "${outdir}/${full_pkgname}.tlz"
603 # Back to the current working directory
604 cd -- "$CWD" || chkstatus_or_exit
606 echo "=== Deleting 'srcdir', 'destdir' ..."
607 if test "$opt_keep" != opt_keep
608 then
609 if test -z "$keep_srcdir"
610 then
611 srcdir="${srcdir%%/*}"; # Directory name without parents.
612 if test -e "${TMPDIR}/$srcdir"
613 then
614 rm -r -- "${TMPDIR:?}/$srcdir" || chkstatus_or_exit
615 echo "removed directory: '${TMPDIR}/$srcdir'"
619 if test -z "$keep_destdir"
620 then
621 if test -e "$destdir"
622 then
623 rm -r -- "$destdir" || chkstatus_or_exit
624 echo "removed directory: '$destdir'"
627 else
628 warn \
629 " Request via --keep to preserve 'srcdir' or 'destdir' has been made:" \
630 "${TMPDIR}/$srcdir" \
631 "$destdir"
634 # Install or upgrade the package after build
635 if test "$opt_nopkg" != opt_nopkg
636 then
637 if test "$opt_install" = opt_install
638 then
639 mode_install "${outdir}/${full_pkgname}.tlz"
640 elif test "$opt_upgrade" = opt_upgrade
641 then
642 mode_upgrade "${outdir}/${full_pkgname}.tlz"
646 echo "{@} All done for \"${CWD}/${recipe}\"."
647 echo ""
650 mode_create()
652 directory="$(dirname -- "$1")"
654 # Perform sanity checks
656 if ! fnmatch '/?*' "$directory"
657 then
658 warn "${PROGRAM}: Output directory \`${directory}' is not fully qualified"
659 exit 4
661 is_readable "$directory" || exit 4
663 name="$(_basename "$1")"
665 echo ""
666 echo "{#} Creating package name \`${name}' ..."
668 if test "$name" = "${name%.tlz}"
669 then
670 warn "Package format '$name' not supported." \
671 "It should be \"name_version_architecture-release[@pkgcategory].tlz\""
672 exit 4
675 ( umask 022 ; tarlz --solid -9 -cvf - -- * ) > "${directory}/$name"
676 chkstatus_or_exit 3
678 ( cd -- "$directory" && sha256sum "$name" > "${name}.sha256" )
679 chkstatus_or_exit 4
681 echo "{#} Package name \"${name}\" created on \`${directory}'."
683 # Remove used variables
684 unset directory name
686 echo ""
689 mode_remove()
691 expunge="${packagedir}/$(_basename "$1" .tlz)"
693 echo ""
694 echo "<<< Removing package \`$rootdir${expunge}' ..."
696 # Complain if the package directory does not exist
698 test -e "$rootdir${expunge}" || {
699 warn "Package directory '$rootdir${expunge}' does not exist."
700 return 4
703 # Complain if the package directory cannot be well-read
705 is_readable "$rootdir${expunge}" || exit 4
707 # Remove package from Graft control
709 # Scan for possible conflicts, stop if arise
710 if test "$opt_prune" != opt_prune
711 then
712 echo "=== Checking for possible conflicts ..."
713 if graft -d -n $graft_r -t "$targetdir" "$expunge" 2>&1 | \
714 grep ^CONFLICT
715 then
716 warn "" \
717 " A conflict occurred during uninstallation;" \
718 "Unless the --prune option is given, this package will be PRESERVED."
719 return 6;
723 # Ignore some signals up to completing the deinstallation
724 trap "" HUP INT QUIT ABRT TERM
726 # Remove objects (files, links or directories) from the target
727 # directory that are in conflict with the package directory
729 echo "=== Pruning any conflict ..."
730 graft -p -D -u $graft_r -t "$targetdir" "$expunge"
731 chkstatus_or_exit 2
733 echo "=== Disabling links ..."
734 graft -d -D -u $graft_v $graft_r -t "$targetdir" "$expunge"
735 chkstatus_or_exit 2
737 # Delete package directory
738 if test "$opt_keep" != opt_keep
739 then
740 echo "=== Deleting package directory, if exists as such ..."
741 if test -e "${rootdir}$expunge"
742 then
743 rm -r -- "${rootdir}$expunge" || chkstatus_or_exit
744 echo "removed directory: '${rootdir}$expunge'"
748 # Reset given signals
749 trap - HUP INT QUIT ABRT TERM
751 # Remove used variables
752 unset expunge
754 echo ""
757 mode_install()
759 echo ""
760 echo ">>> Installing package \`${1}' ..."
762 # Complain if the package cannot be well-read
764 is_readable "$1" || exit 4
766 # Complain if the package does not end in .tlz
768 if ! fnmatch '*.tlz' "$1"
769 then
770 warn "\`${1}' does not end in .tlz"
771 return 4
774 # Make preparations to install the package
776 # Get the filename
777 name="$(_basename "$1" .tlz)"
779 echo "=== Checking tarball integrity ..."
780 tarlz --missing-crc -tf "$1" > /dev/null
781 chkstatus_or_exit 3
783 # To accept random directory from the upgrade mode
784 _packagedir="${_packagedir:=$packagedir}"
786 # Create package directory using 'name'
787 if ! test -d "$rootdir${_packagedir}/$name"
788 then
789 mkdir -p -- "$rootdir${_packagedir}/$name" || chkstatus_or_exit
790 echo "mkdir: created directory '$rootdir${_packagedir}/$name'"
793 # Scan for possible conflicts, stop if arise
794 if test "$opt_prune" != opt_prune
795 then
796 echo "=== Checking for possible conflicts ..."
797 if graft -i -n $graft_r -t "$targetdir" "${_packagedir}/$name" 2>&1 | \
798 grep ^CONFLICT
799 then
800 warn "" \
801 " A conflict occurred during installation;" \
802 "Unless the --prune option is given, this package won't be LINKED."
803 return 6;
807 # Ignore some signals up to completing the installation
808 trap "" HUP INT QUIT ABRT TERM
810 echo "=== Decompressing package ..."
811 ( cd -- "$rootdir${_packagedir}/$name" && tarlz -xf - ) < "$1"
812 chkstatus_or_exit 3
814 # Transite package to Graft control
816 # Remove objects (files, links or directories) from the target
817 # directory that are in conflict with the package directory
818 echo "=== Pruning any conflict ..."
819 graft -p -D -u $graft_r -t "$targetdir" "${_packagedir}/$name"
820 chkstatus_or_exit 2
822 echo "=== Enabling symbolic links ..."
823 graft -i -P $graft_v $graft_r -t "$targetdir" "${_packagedir}/$name"
824 chkstatus_or_exit 2
826 # Avoid unnecessary runs coming from the upgrade mode,
827 # this is when the incoming package is **pre-installed**
829 if test "$_isUpgrade" != _isUpgrade.on
830 then
831 # Show package description
832 if test -r "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.txt"
833 then
834 awk '/^#/' "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.txt"
835 elif test -r "${1}.txt"
836 then
837 # From external meta file (current directory)
838 awk '/^#/' "${1}.txt"
839 else
840 warn "Description file not found for '$name'."
843 # Check and run the post-install script if exist
844 if test -r "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.sh"
845 then
846 echo "=== Running post-install script for \`${name}' ..."
848 # Rely on 'targetdir' if 'rootdir' is empty
849 cd -- "${rootdir:=$targetdir}"/ && \
850 . "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.sh"
854 # Check if there are declared packages for replacement
855 if test -r "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.replace"
856 then
857 while read -r line
859 for replace in "$rootdir${_packagedir}/$(pkgbase "$line")"_*
861 if ! test -e "$replace"
862 then
863 warn "<^> Declared package \`${replace}' does not exist. (ignoring)"
864 continue;
867 replace="${replace##*/}"
869 # The search for the package to be replaced cannot
870 # be the same to the incoming package, even to the
871 # temporary location coming from the upgrade mode
872 if test "$replace" = "$name" || \
873 test "_x_${replace}" = "_x_${PRVLOC##*/}"
874 then
875 continue;
878 warn "WARNING: Replacing package \`${replace}' ..."
880 # Since the links belongs to the new package, only
881 # those which are not in conflict can be deleted.
882 # To complete, we will remove the package directory
884 graft -d -D -u $graft_r \
885 -t "$targetdir" "$replace" > /dev/null 2>&1
887 rm -rf -- "$rootdir${_packagedir}/$replace"
888 done
889 done < "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.replace"
890 unset line
894 # Reset given signals
895 trap - HUP INT QUIT ABRT TERM
897 # Remove used variables
898 unset name _packagedir
900 echo ""
903 mode_order()
905 # Complain if the file cannot be well-read
907 is_readable "$1" || exit 4
909 # Complain if the file does not end in .order
911 if ! fnmatch '*.order' "$1"
912 then
913 warn "\`${1}' does not end in .order"
914 return 4
917 # Get a clean list of the file while printing its contents in reverse
918 # order. The last `awk 'in the pipeline eliminates the non-consecutive
919 # lines, the duplicates. Blank lines, colons and parentheses are
920 # simply ignored, comment lines beginning with '#' are allowed
922 awk \
923 '{ gsub( /:|^#(.*)$|\([^)]*)|^$/,"" ); for( i=NF; i > 0; i-- ) print $i }' \
924 "$1" | awk '!s[$0]++'
927 mode_upgrade()
929 # Complain if the package does not end in .tlz
931 if ! fnmatch '*.tlz' "$1"
932 then
933 warn "\`${1}' does not end in .tlz"
934 return 4
937 # Get the filename
938 incoming="$(_basename "$1" .tlz)"
940 echo ""
941 echo "{%} Upgrading package \`${incoming}' ..."
943 # Check package pre-existence
944 if test "$opt_force" != opt_force && \
945 test -e "$rootdir${packagedir}/$incoming"
946 then
947 warn \
948 "" \
949 " The package to be upgraded already exist;" \
950 "Unless the --force option is given, this package won't be UPGRADED."
951 return 6;
954 # Check blacklisted packages
956 echo "=== Checking blacklist ..."
957 for item in $blacklist
959 case $item in
960 ${incoming}*)
961 warn \
962 "Package name declared on the blacklist \"${incoming}\"." \
963 "" \
964 " This package will be INSTALLED instead of being upgraded."
965 opt_prune=opt_prune mode_install "$1"
966 return 0
968 esac
969 done
970 unset item
972 # Prepare the package to install it in a temporary but custom location
974 # Set random directory under 'rootdir/packagedir' using 'incoming' as name
975 PRVLOC=$(mktemp -dp "$rootdir${packagedir}" ${incoming}.XXXXXXXXXXXX) || exit 2
977 echo "=== Pre-installing package in temporary location ..."
978 opt_prune=opt_prune # Turn on prune operation.
979 _isUpgrade=_isUpgrade.on mode_install "$1" "$PRVLOC" > /dev/null
980 _isUpgrade=_isUpgrade.off
982 echo "=== Looking for installations under the same name ..."
983 for long_name in "$rootdir${packagedir}/$(pkgbase "$incoming")"*
985 found="${long_name##*/}"
987 # The search for the package to be deleted
988 # cannot be the same to the temporary location
989 test "$long_name" = "$PRVLOC" && continue;
991 fnmatch "$(pkgbase "$found")*" "$incoming" || continue;
992 echo "${long_name} has been detected."
994 # A package directory will be preserved if --keep is given
995 mode_remove "$found" > /dev/null
996 done
997 unset long_name found
999 # Re-install the package removing the temporary location
1001 mode_install "$1"
1002 opt_prune=opt_prune.off # Turn off prune operation.
1004 echo "=== Deleting temporary location ..."
1005 rm -rf -- "$PRVLOC" || chkstatus_or_exit
1006 echo "removed directory: '$PRVLOC'"
1008 echo "{%} Upgraded from \"${1}\"."
1010 # Remove remaining variables
1011 unset incoming PRVLOC
1013 echo ""
1016 mode_warn()
1018 # Complain if the package cannot be well-read
1020 is_readable "$1" || exit 4
1022 # Complain if the package does not end in .tlz
1024 if ! fnmatch '*.tlz' "$1"
1025 then
1026 warn "\`${1}' does not end in .tlz"
1027 return 4
1030 # List content of files excluding directories
1031 while test -f "$1"
1033 tarlz -tvvf "$1" | awk '!/^drwx/'
1034 chkstatus_or_exit 3
1035 shift;
1036 done
1037 return 0
1040 mode_extract()
1042 # Perform sanity checks before package extraction
1044 is_readable "$1" || exit 4
1046 test -f "$1" || {
1047 warn "\`${1}' is not a regular file."
1048 return 4
1051 # Preparations to extract the package
1053 name="$(_basename "$1" .tlz)"
1055 # Set random directory under 'TMPDIR' using 'name'
1056 PRVDIR=$(mktemp -dp "$TMPDIR" ${name}.XXXXXXXXXXXX) || exit 2
1058 # Trap to remove 'PRVDIR' on disruptions
1059 trap 'rm -rf -- "$PRVDIR"' HUP INT ABRT TERM
1061 # Create 'PRVDIR' removing access for all but user
1062 ( umask 077 ; mkdir -- "$PRVDIR" )
1063 mkdir -p -- "$PRVDIR"
1064 chmod 700 -- "$PRVDIR"
1066 echo ""
1067 echo "--- Extracting package \`${name}' ..."
1069 ( umask 000 ; cd -- "$PRVDIR" && tarlz -xvf - ) < "$1"
1070 if test $? -ne 0
1071 then
1072 # Try to remove (empty) 'PRVDIR' on failure
1073 rmdir -- "$PRVDIR"
1074 exit 3;
1077 echo ""
1078 echo "\"${name}\" has been extracted on \`${PRVDIR}'"
1080 # Reset given signals
1081 trap - HUP INT ABRT TERM
1083 # Remove used variables
1084 unset name PRVDIR
1087 ### Extra functions to be used during the modes
1089 pkgbase()
1091 printf '%s' "${1%%_*}"
1094 unpack()
1096 for file in "$@"
1098 case $file in
1099 *.tar)
1100 tar -tf "$file" > /dev/null && \
1101 tar -xpf "$file"
1102 chkstatus_or_exit 3
1104 *.tar.gz | *.tgz | *.tar.Z )
1105 gzip -cd "$file" | tar -tf - > /dev/null && \
1106 gzip -cd "$file" | tar -xpf -
1107 chkstatus_or_exit 3
1109 *.tar.bz2 | *.tbz2 | *.tbz )
1110 bzip2 -cd "$file" | tar -tf - > /dev/null && \
1111 bzip2 -cd "$file" | tar -xpf -
1112 chkstatus_or_exit 3
1114 *.tar.lz | *.tlz )
1115 lzip -cd "$file" | tar -tf - > /dev/null && \
1116 lzip -cd "$file" | tar -xpf -
1117 chkstatus_or_exit 3
1119 *.tar.xz | *.txz )
1120 xz -cd "$file" | tar -tf - > /dev/null && \
1121 xz -cd "$file" | tar -xpf -
1122 chkstatus_or_exit 3
1124 *.zip | *.ZIP )
1125 unzip -t "$file" > /dev/null && \
1126 unzip "$file" > /dev/null
1127 chkstatus_or_exit 3
1129 *.gz)
1130 gzip -t "$file" && \
1131 gzip -cd "$file" > "$(_basename "$file" .gz)"
1132 chkstatus_or_exit 3
1134 *.Z)
1135 gzip -t "$file" && \
1136 gzip -cd "$file" > "$(_basename "$file" .Z)"
1137 chkstatus_or_exit 3
1139 *.bz2)
1140 bzip2 -t "$file" && \
1141 bzip2 -cd "$file" > "$(_basename "$file" .bz2)"
1142 chkstatus_or_exit 3
1144 *.lz)
1145 lzip -t "$file" && \
1146 lzip -cd "$file" > "$(_basename "$file" .lz)"
1147 chkstatus_or_exit 3
1149 *.xz)
1150 xz -t "$file" && \
1151 xz -cd "$file" > "$(_basename "$file" .xz)"
1152 chkstatus_or_exit 3
1155 warn "${PROGRAM}: cannot unpack ${file}: Unsupported extension"
1156 exit 1
1157 esac
1158 done
1159 unset file
1162 do_meta()
1164 # Extract information from the recipe to create the meta file.
1166 # The package description is pre-formatted in 78 columns,
1167 # the '#' character and a space is added as prefix to conform
1168 # the 80 columns in total
1170 cat << EOF
1171 $(printf '%s\n' "$description" | fold -w 78 | awk '$0="# " $0')
1173 QICFLAGS="$QICFLAGS"
1174 QICXXFLAGS="$QICXXFLAGS"
1175 QILDFLAGS="$QILDFLAGS"
1176 pkgname=$pkgname
1177 pkgversion=$pkgversion
1178 arch=$arch
1179 release=$release
1180 pkgcategory="${pkgcategory#@*}"
1181 full_pkgname=$full_pkgname
1182 blurb="$(echo "$description" | sed -e '/^$/d;2q')"
1183 homepage="$homepage"
1184 license="$license"
1185 fetch="$fetch"
1186 replace="$replace"
1191 ### Default values
1193 packagedir=@PACKAGEDIR@
1194 targetdir=@TARGETDIR@
1195 blacklist="perl5 graft tarlz plzip musl glibc coreutils bash mksh"
1196 readconfig=readconfig
1197 _rcfile=@SYSCONFDIR@/qirc
1198 opt_install=opt_install.off
1199 opt_upgrade=opt_upgrade.off
1200 opt_force=opt_force.off
1201 opt_keep=opt_keep.off
1202 opt_incr_release=opt_incr_release.off
1203 opt_skipqsts=opt_skipqsts.off
1204 opt_nopkg=opt_nopkg.off
1205 opt_prune=opt_prune.off
1206 verbose=0
1207 rootdir=""
1208 arch=@ARCH@
1209 jobs=1
1210 mode=""
1211 readstdin=""
1212 graft_v=""
1213 graft_r=""
1214 _isUpgrade=_isUpgrade.off
1215 keep_srcdir=""
1216 keep_destdir=""
1217 TMPDIR="${TMPDIR:=/usr/src/qi/build}"
1218 QICFLAGS="${QICFLAGS:=-g0 -Os}"
1219 QICXXFLAGS="${QICXXFLAGS:=-g0 -Os}"
1220 QILDFLAGS="${QILDFLAGS:=-s}"
1221 worktree=/usr/src/qi
1222 tardir=${worktree}/sources
1223 outdir=/var/cache/qi/packages
1224 netget="wget -c -w1 -t3 --no-check-certificate"
1225 rsync="rsync -v -a -L -z -i --progress"
1226 configure_args="--prefix=@PREFIX@ --libexecdir=@LIBEXECDIR@ --bindir=@BINDIR@ --sbindir=@SBINDIR@ --sysconfdir=@SYSCONFDIR@ --localstatedir=@LOCALSTATEDIR@"
1227 infodir=@INFODIR@
1228 mandir=@MANDIR@
1229 docdir=@DOCDIR@
1231 # Store (default) directory locations
1232 QI_TARGETDIR=$targetdir
1233 QI_PACKAGEDIR=$packagedir
1234 QI_WORKTREE=$worktree
1235 QI_TARDIR=$tardir
1236 QI_OUTDIR=$outdir
1238 ### Parse commands and options
1240 _validate_mode()
1242 if test -n "$mode"
1243 then
1244 warn "${PROGRAM}: First defined command: ${mode#*_}" \
1245 "Switching to another command is not allowed (${1})."
1246 exit 1
1250 _validate_option()
1252 if test -z "$2"
1253 then
1254 warn "${PROGRAM}: The '${1}' option requires an argument" \
1255 "Try '${PROGRAM} --help' for more information."
1256 exit 1
1260 _validate_directory()
1262 if test ! -d "$2"
1263 then
1264 warn "${PROGRAM}: ${1} \"${2}\" must be a valid directory name"
1265 exit 1
1269 _validate_digit()
1271 name="$1"
1272 shift
1274 # Taken from https://mywiki.wooledge.org/BashFAQ/054
1275 case ${1#[-+]} in
1277 warn "${PROGRAM}: The '${name}' option has no defined value"
1278 exit 1
1280 *.*.*)
1281 warn "${PROGRAM}: The '${name}' option has more than one decimal point on it \"${1}\""
1282 exit 1
1284 *[!0-9]*)
1285 warn "${PROGRAM}: The '${name}' option contains a non-valid digit on it \"${1}\""
1286 exit 1
1288 esac
1290 unset name
1293 while test $# -gt 0
1295 case $1 in
1296 warn)
1297 _validate_mode warn
1298 mode=mode_warn
1300 install)
1301 _validate_mode install
1302 readconfig
1303 mode=mode_install
1305 remove)
1306 _validate_mode remove
1307 readconfig
1308 mode=mode_remove
1310 upgrade)
1311 _validate_mode upgrade
1312 readconfig
1313 mode=mode_upgrade
1315 extract)
1316 _validate_mode extract
1317 readconfig
1318 mode=mode_extract
1320 create)
1321 _validate_mode create
1322 readconfig
1323 mode=mode_create
1325 order)
1326 _validate_mode order
1327 readconfig
1328 mode=mode_order
1330 build)
1331 _validate_mode build
1332 readconfig
1333 mode=mode_build
1335 --no-rc | -N )
1336 readconfig=readconfig.off
1338 --install | -i )
1339 opt_install=opt_install
1341 --upgrade | -u )
1342 opt_upgrade=opt_upgrade
1344 --force | -f )
1345 opt_force=opt_force
1347 --keep | -k )
1348 opt_keep=opt_keep
1350 --prune | -p )
1351 opt_prune=opt_prune
1353 --packagedir | -P )
1354 _validate_option "$1" "$2"
1355 packagedir="$2"
1356 _validate_directory "$1" "$packagedir"
1357 shift
1359 --packagedir=*)
1360 _validate_option "$1" "$2"
1361 packagedir="${1#*=}"
1362 _validate_directory "$1" "$packagedir"
1364 --targetdir | -t )
1365 _validate_option "$1" "$2"
1366 targetdir="$2"
1367 _validate_directory "$1" "$targetdir"
1368 shift
1370 --targetdir=*)
1371 _validate_option "$1" "$2"
1372 targetdir="${1#*=}"
1373 _validate_directory "$1" "$targetdir"
1375 --rootdir | -r )
1376 _validate_option "$1" "$2"
1377 rootdir="$2"
1378 _validate_directory "$1" "$rootdir"
1379 shift
1381 --rootdir=*)
1382 _validate_option "$1" "$2"
1383 rootdir="${1#*=}"
1384 _validate_directory "$1" "$rootdir"
1386 --outdir | -o )
1387 _validate_option "$1" "$2"
1388 outdir="$2"
1389 _validate_directory "$1" "$outdir"
1390 shift
1392 --outdir=*)
1393 _validate_option "$1" "$2"
1394 outdir="${1#*=}"
1395 _validate_directory "$1" "$outdir"
1397 --worktree | -w )
1398 _validate_option "$1" "$2"
1399 worktree="$2"
1400 _validate_directory "$1" "$worktree"
1401 shift
1403 --worktree=*)
1404 _validate_option "$1" "$2"
1405 worktree="${1#*=}"
1406 _validate_directory "$1" "$worktree"
1408 --sourcedir | -s )
1409 _validate_option "$1" "$2"
1410 tardir="$2"
1411 _validate_directory "$1" "$tardir"
1412 shift
1414 --sourcedir=*)
1415 _validate_option "$1" "$2"
1416 tardir="${1#*=}"
1417 _validate_directory "$1" "$tardir"
1419 --architecture | -a )
1420 _validate_option "$1" "$2"
1421 arch="$2"
1422 shift
1424 --arch=*)
1425 _validate_option "$1" "$2"
1426 arch="${1#*=}"
1428 --jobs | -j )
1429 jobs="$2"
1430 _validate_digit "$1" "$jobs"
1431 shift
1433 -j[0-9]*)
1434 jobs="${1#-j*}"
1435 _validate_digit '-j' "$jobs"
1437 --jobs=*)
1438 jobs="${1#*=}"
1439 _validate_digit '--jobs=' "$jobs"
1441 --no-package | -n )
1442 opt_nopkg=opt_nopkg
1444 --increment | -1 )
1445 opt_incr_release=opt_incr_release
1447 --skip-questions | -S )
1448 opt_skipqsts=opt_skipqsts
1450 --verbose | -v )
1451 verbose=$(( verbose + 1 ))
1453 -vv)
1454 # ^ Trick for a second -v.
1455 verbose=2
1457 --show-location | -L )
1458 printf '%s\n' \
1459 "QI_TARGETDIR=$QI_TARGETDIR" \
1460 "QI_PACKAGEDIR=$QI_PACKAGEDIR" \
1461 "QI_WORKTREE=$QI_WORKTREE" \
1462 "QI_TARDIR=$QI_TARDIR" \
1463 "QI_OUTDIR=$QI_OUTDIR"
1464 exit 0
1466 --help | --hel | --he | --h | '--?' | -help | -hel | -he | -h | '-?' | \
1467 help )
1468 usage
1469 exit 0
1471 --version | --versio | --versi | --vers | \
1472 -version | -versio | -versi | -vers | -V | version )
1473 echo "$PROGRAM version @VERSION@"
1474 exit 0
1476 '-')
1477 readstdin=readstdin
1478 break
1481 shift
1482 break; # End of options.
1485 warn "qi: Unrecognized option: $1" \
1486 "Try '${PROGRAM} --help' for more information."
1487 exit 1
1490 break; # No more options.
1492 esac
1493 shift
1494 done
1495 unset _validate_mode _validate_option _validate_directory _validate_digit
1497 # When there are no arguments, show the help
1498 if test $# -eq 0
1499 then
1500 usage
1501 exit 1
1504 # Program sanity check
1506 \unalias -a; # Unset all possible aliases.
1507 unset -f command; # Ensure command is not a user function.
1509 for need in awk chmod cp dirname find fold graft grep mkdir \
1510 mktemp rm rmdir sed sha256sum stat tarlz ; \
1512 if ! command -v $need > /dev/null
1513 then
1514 warn "${PROGRAM}: Prerequisite \`${need}' not found in PATH"
1515 exit 2
1517 done
1518 unset need
1520 # Set verbosity level/flag
1522 if test "$verbose" -gt 0
1523 then
1524 if test "$verbose" -eq 1
1525 then
1526 graft_v=-v
1527 else
1528 graft_v=-V
1532 # Read standard input if FILE is -, or when
1533 # FILE is not connected to a terminal
1535 if test "$readstdin" = readstdin
1536 then
1537 if test -t 0
1538 then
1539 warn "qi: I won't read from a connected terminal." \
1540 "Try '${PROGRAM} --help' for more information."
1541 exit 1
1544 # Unset positional parameters setting $# to zero
1545 set --
1547 # Assign remaining arguments to the positional parameters
1548 while read -r input
1550 set -- "$@" "$input"
1551 done
1553 unset readstdin
1555 if test -z "$mode"
1556 then
1557 warn "qi: We need at least one (valid) command." \
1558 "Try '${PROGRAM} --help' for more information."
1559 exit 4
1562 # Validate 'packagedir' and 'targetdir' as canonical directories
1564 # The single slash '/' does not qualify here
1565 if ! fnmatch '/?*' "$packagedir"
1566 then
1567 warn "${PROGRAM}: Package directory \`${packagedir}' is not fully qualified"
1568 exit 4
1570 if test ! -d "$packagedir"
1571 then
1572 warn "${PROGRAM}: Package directory \`${packagedir}' does not exist"
1573 exit 4
1576 # The single slash '/' is valid here
1577 if ! fnmatch '/*' "$targetdir"
1578 then
1579 warn "${PROGRAM}: Target directory \`${targetdir}' is not fully qualified"
1580 exit 4
1582 if test ! -d "$targetdir"
1583 then
1584 warn "${PROGRAM}: Target directory \`${targetdir}' does not exist"
1585 exit 4
1588 # Validate 'rootdir' directory
1590 if test -n "$rootdir"
1591 then
1592 if test -d "$rootdir" && test "$rootdir" != /
1593 then
1594 rootdir="${rootdir%/}" # Remove slash from the end.
1596 # A workaround for graft-2.13+. The specified directory is
1597 # relative to the log file, we prepend it inside 'rootdir'
1599 eval "$(graft -L)" ; GRAFT_LOGFILE="${GRAFT_LOGFILE:=/var/log/graft}"
1600 mkdir -p -- "$rootdir$(dirname -- "$GRAFT_LOGFILE")" || chkstatus_or_exit
1602 # Compose 'rootdir' and log file option to be used with graft(1)
1603 graft_r="-r $rootdir -l $GRAFT_LOGFILE"
1605 # Unset variables coming from eval
1606 unset GRAFT_PERL GRAFT_LOGFILE GRAFT_TARGETDIR GRAFT_PACKAGEDIR
1607 else
1608 warn "${PROGRAM}: Root directory \`${rootdir}' is not fully qualified"
1609 exit 4
1611 readonly rootdir
1612 export rootdir
1615 # Ensure 'TMPDIR' creation to prefix temporary files
1617 if test ! -d "$TMPDIR"
1618 then
1619 mkdir -p -- "$TMPDIR" || chkstatus_or_exit
1621 readonly TMPDIR packagedir targetdir
1623 # Process each package or recipe provided on the command-line
1625 for package in "$@"
1627 $mode "$package"
1628 done