qi: Bump to release 2.4
[dragora.git] / qi / src / qi.in
blob2a4fc62924c1f704f8379f8b81ee731224e462a3
1 #! /bin/sh -
2 # Copyright (C) 2016-2021 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' \
38 "Usage: $PROGRAM COMMAND [OPTIONS] [FILE]...
39 "'A simple but well-integrated package manager.
41 List of commands:
42 warn Warn about files that will be installed
43 install Install packages
44 remove Remove packages
45 upgrade Upgrade packages
46 extract Extract packages for debugging purposes
47 create Create a .tlz package from directory
48 build Build packages using recipe names
49 order Resolve build order through .order files
51 Options when installing, removing, or upgrading software packages:
52 -f, --force Force upgrade of pre-existing packages
53 -k, --keep Keep package directory when remove/upgrade
54 -p, --prune Prune conflicts
55 -P, --packagedir=<dir> Set directory for package installations
56 -t, --targetdir=<dir> Set target directory for symbolic links
57 -r, --rootdir=<dir> Use the fully qualified named directory as
58 the root directory for all qi operations
59 Note: the target directory and the package
60 directory will be relative to the specified
61 directory, excepting the graft log file
63 Options when building software packages using recipes:
64 -a, --architecture Set architecture name for the package
65 -j, --jobs Parallel jobs for the compiler
66 -k, --keep Keep ${srcdir} or ${destdir} when build
67 -S, --skip-questions Skip questions on completed recipes
68 -1, --increment Increment release number (${release} + 1)
69 -n, --no-package Do not create a .tlz package
70 -i, --install Install package after the build
71 -u, --upgrade Upgrade package after the build
72 -o, --outdir=<dir> Where the packages produced will be written
73 -w, --worktree=<dir> Where archives, patches, recipes are expected
74 -s, --sourcedir=<dir> Where compressed sources will be found
76 Other options:
77 -N, --no-rc Do not read the configuration file
78 -v, --verbose Be verbose (an extra -v gives more)
79 -L, --show-location Print default directory locations and exit
80 -h, --help Display this help and exit
81 -V, --version Output version information and exit
83 Some influential environment variables:
84 TMPDIR Temporary directory for sources
85 QICFLAGS C compiler flags (to be used on CFLAGS)
86 QICXXFLAGS C++ compiler flags (to be used on CXXFLAGS)
87 QILDFLAGS Flags for the linker (to be used on LDFLAGS)
88 QICPPFLAGS C/C++ preprocessor flags (to be used on CPPFLAGS)
89 SOURCE_DATE_EPOCH Last modification time for created packages
91 When FILE is -, read standard input.
93 Exit status: 0 for a normal exit, 1 for minor common errors (help usage,
94 support not available, etc), 2 to indicate a command execution error;
95 3 for integrity check error on compressed files, 4 for empty, not
96 regular, or expected files, 5 for empty or not defined variables,
97 6 when a package already exist, 10 for network manager errors.
99 Qi home page: https://www.dragora.org
103 warn()
105 printf '%s\n' "$@" 1>&2
108 is_readable()
110 if test -e "$1"
111 then
112 if ! test -r "$1"
113 then
114 echo "${PROGRAM}: cannot read ${1}: Permission denied" 1>&2
115 return 1
117 else
118 echo "${PROGRAM}: cannot access ${1}: No such file or directory" 1>&2
119 return 1
123 # Portable alternative to the file operator -nt (among shells)
124 is_newer()
126 if test -n "$(find "$1" -prune -newer "$2" -print)"
127 then
128 return 0
130 return 1
133 # Determine whether $2 matches pattern $1
134 fnmatch()
136 case $2 in
138 return 0
141 return 1
143 esac
146 chkstatus_or_exit()
148 status=$?
150 if test $status -ne 0
151 then
152 echo "^ Return status = $status" 1>&2
153 exit "${1-2}"; # If not given, defaults to 2
156 unset -v status
159 readconfig()
161 if test "${_readconfig:-readconfig}" = readconfig
162 then
163 is_readable "$HOME/.qirc" 2> /dev/null && _rcfile="$HOME/.qirc";
164 echo "Importing configuration file from \`${_rcfile}' ..."
165 . "$_rcfile" || chkstatus_or_exit 5
167 unset -v _readconfig
170 ### Mode functions
172 mode_build()
174 recipe=$1
176 echo ""
177 echo "{@} Building \"${recipe}\" ..."
179 # A recipe is any valid regular file. Qi sets priorities for reading
180 # a recipe, the order in which qi looks for a recipe is:
182 # 1. Current working directory.
184 # 2. If the specified path name does not contain "recipe" as the last
185 # component. Qi will complete it by adding "recipe" to the path
186 # name.
188 # 3. If the recipe is not in the current working directory, it will be
189 # searched under '${worktree}/recipes'. The last component will be
190 # completed adding "recipe" to the specified path name.
192 if test ! -f "$recipe"
193 then
194 if test -f "${recipe}/recipe"
195 then
196 recipe="${recipe}/recipe"
197 elif test -f "${worktree}/recipes/${recipe}/recipe"
198 then
199 recipe="${worktree}/recipes/${recipe}/recipe"
203 test -f "$recipe" || {
204 warn "\`${recipe}' is not a regular file."
205 return 4
208 # Complain if the file name is not "recipe"
210 if test "${recipe##*/}" != recipe
211 then
212 warn "\`${recipe}' is not a valid recipe name."
213 return 4
216 # Start preparations to import the recipe
218 # Get working directory and base name of the recipe
220 CWD="$(CDPATH='' cd -P -- "$(dirname -- "$recipe")" && pwd -P)"
221 recipe="$(basename -- "$recipe")"
223 # Check readability for load the recipe on success
225 is_readable "${CWD}/$recipe" || exit 4
227 # Re-create external directories
228 mkdir -p -- "${worktree}/archive" \
229 "${worktree}/patches" \
230 "${worktree}/recipes" \
231 "$tardir"
233 # Variables treatment for the current and the next recipe.
235 # Unset special variables that can only be predefined in
236 # the recipe and does not come from '${sysconfdir}/qirc'
238 unset -v \
239 srcdir destdir pkgname pkgversion pkgcategory program version release \
240 fetch description homepage license replace full_pkgname docs docsdir \
241 CFLAGS CXXFLAGS LDFLAGS CPPFLAGS
243 # The following variables must be restored, later
244 save_arch="${save_arch:=$arch}"
245 save_jobs="${save_jobs:=$jobs}"
246 save_outdir="${save_outdir:=$outdir}"
247 save_opt_nopkg="${save_opt_nopkg:=$opt_nopkg}"
249 # Reset variable values in case of return
250 arch="$save_arch"
251 jobs="$save_jobs"
252 outdir="$save_outdir"
253 opt_nopkg="$save_opt_nopkg"
255 # The following variables cannot be redefined on the recipe
256 readonly worktree netget rsync
258 # Import the recipe
260 . "${CWD}/$recipe"
261 chkstatus_or_exit
263 # Check for required variables
264 if test -z "$program"
265 then
266 warn "${recipe}: The variable 'program' is not defined."
267 exit 5
269 if test -z "$version"
270 then
271 warn "${recipe}: The variable 'version' is not defined."
272 exit 5
274 if test -z "$release"
275 then
276 warn "${recipe}: The variable 'release' is not defined."
277 exit 5
280 # Pre-settings before to start building
282 # Increment the release number if the option was given
283 if test "$opt_incr_release" = opt_incr_release
284 then
285 release=$(( release + 1 ))
288 # Allow the dot as definition for 'tardir'
289 if test "$tardir" = .
290 then
291 tardir="$CWD"
294 # Set default values for the following special variables
296 pkgname="${pkgname:=$program}"
297 pkgversion="${pkgversion:=$version}"
298 srcdir="${srcdir:=${program}-$version}"
299 destdir="${destdir:=${TMPDIR}/package-$pkgname}"
301 # If 'pkgcategory' has been defined, prefix it using the "at" symbol
302 if test -n "$pkgcategory"
303 then
304 pkgcategory="@${pkgcategory}"
307 # Compose the full package name
308 full_pkgname="${full_pkgname:=${pkgname}_${pkgversion}_${arch}-${release}${pkgcategory}}"
310 # Use 'arch' as suffix for 'outdir' to have a well-organized package output
311 outdir="${outdir}/${arch}"
313 # If a package is going to be created the existence of a
314 # previous build will be detected and reported. Under normal
315 # conditions the recipe is built as long as it is newer than
316 # the produced package, if not, we warn to the user about it.
317 # Rebuilding the package is possible (through the force ;-)
319 if test "$opt_nopkg" != opt_nopkg && \
320 { test "$opt_force" != opt_force && \
321 test -e "${outdir}/${full_pkgname}.tlz" ; }
322 then
323 if is_newer "${CWD}/$recipe" "${outdir}/${full_pkgname}.tlz"
324 then
325 warn \
326 "" \
327 "This recipe is more RECENT than the produced package:" \
328 "" \
329 "$( stat -c "%y %n" "${CWD}/$recipe" )" \
330 "$( stat -c "%y %n" "${outdir}/${full_pkgname}.tlz" )" \
331 "" \
332 " This recipe will be processed ..." \
334 elif test -e "${CWD}/post-install" && \
335 is_newer "${CWD}/post-install" "${CWD}/$recipe"
336 then
337 warn \
338 "" \
339 "The post-install script is more RECENT than the recipe:" \
340 "" \
341 "$( stat -c "%y %n" "${CWD}/post-install" )" \
342 "$( stat -c "%y %n" "${CWD}/$recipe" )" \
343 "" \
344 " This recipe will be re-processed ..." \
346 touch "${CWD}/$recipe"
347 elif test "$opt_skipqsts" = opt_skipqsts
348 then
349 warn "Recipe for '${full_pkgname}.tlz': [Ignored]." ""
350 return 0
351 else
352 warn \
353 "" \
354 "This recipe ALREADY produced a package:" \
355 "$( stat -c "%y %n" "${outdir}/${full_pkgname}.tlz" )" \
356 "" \
357 "The recipe is still OLDER than the produced package:" \
358 "$( stat -c "%y %n" "${CWD}/$recipe" )" \
359 "" \
360 " Probably nothing has changed." \
363 # In non-interactive mode, the user is asked about
364 # rebuilding the package. In interactive mode,
365 # the user need to pass the option explicitly
366 if test ! -t 0
367 then
368 printf '%s\n' \
369 "Do you want to rebuild this package?" \
370 "1) Yes, built it" \
371 "2) No, skip it [default]" \
372 "3) Resume, skipping completed recipes" \
373 "Enter an option number:" > /dev/tty
374 IFS= read -r ANSWER < /dev/tty || exit 2;
375 case $ANSWER in
377 echo "$ANSWER" > /dev/tty
378 unset -v ANSWER
381 unset -v ANSWER
382 echo "=== Building UNPROCESSED/MODIFIED recipes ..." > /dev/tty
383 echo ""
384 opt_skipqsts=opt_skipqsts
385 readonly opt_skipqsts
386 return 0
389 unset -v ANSWER
390 echo "Recipe for '${full_pkgname}.tlz': Cancelled." > /dev/tty
391 echo ""
392 return 0
394 esac
395 else
396 warn "Use the --force option to reprocess ${CWD}/$recipe." ""
397 return 6;
402 # Fetch remote sources
404 echo "=== Fetching remote sources if needed ..."
405 if test -n "$fetch"
406 then
407 for origin in $fetch
409 _source="${origin##*/}"
411 echo "=== Looking for \"$_source\" ..."
413 echo "=== Verifying checksum file \`${tardir}/${_source}.sha256'"
414 if test -e "${tardir}/${_source}.sha256"
415 then
416 ( cd -- "$tardir" && sha256sum - ) < "${tardir}/${_source}.sha256"
417 chkstatus_or_exit
418 continue;
419 else
420 warn "${_source}.sha256: Checksum file does not exist, yet."
423 # Download source or resume if allowed
425 if test ! -e "${tardir}/$_source"
426 then
427 warn " Can't find \"${_source}\" at \"${tardir}\";" \
428 "attempting to get it from ${origin%/*} ..."
431 case $origin in
432 rsync://*)
434 cd -- "$tardir" && $rsync "$origin" || exit $?
435 sha256sum "$_source" > "${_source}.sha256"
436 ); chkstatus_or_exit 10
438 *://*)
440 cd -- "$tardir" && $netget "$origin" || exit $?
441 sha256sum "$_source" > "${_source}.sha256"
442 ); chkstatus_or_exit 10
445 warn "${PROGRAM}: Unrecognized protocol for ${origin}."
446 exit 4
447 esac
448 done
449 unset -v origin _source
450 else
451 warn "${recipe}: The variable 'fetch' is empty."
454 # Prepare special directories for build the source,
455 # the destination and the output of the package
457 echo "=== Preparing directories ..."
459 if test -z "$keep_srcdir"
460 then
461 if test -e "${TMPDIR}/$srcdir"
462 then
463 rm -r -- "${TMPDIR:?}/$srcdir" || chkstatus_or_exit
464 echo "removed directory: '${TMPDIR}/$srcdir'"
466 else
467 warn \
468 "WARNING: The variable 'keep_srcdir' has been set (${keep_srcdir})."
471 if test -z "$keep_destdir"
472 then
473 if test -e "$destdir"
474 then
475 rm -r -- "$destdir" || chkstatus_or_exit
476 echo "removed directory: '$destdir'"
478 mkdir -p -- "$destdir" || chkstatus_or_exit
479 echo "mkdir: created directory '$destdir'"
482 if test ! -e "$outdir"
483 then
484 mkdir -p -- "$outdir" || chkstatus_or_exit
485 echo "mkdir: created directory '$outdir'"
488 echo "=== Changing to '${TMPDIR}' ..."
489 cd -- "$TMPDIR" || chkstatus_or_exit
491 # Set trap before to run the build() function in order
492 # to catch the return status, exit code 2 if fails
494 trap 'chkstatus_or_exit 2' EXIT HUP INT QUIT ABRT TERM
496 # Determine if the debugging indicators of the shell should be
497 # retained, assuming that it has been previously passed
498 case $- in *x*)
499 _xtrace_flag=_xtrace_flag_is_set ;;
500 esac
502 echo "=== Running the 'build' function ..."
503 build
504 unset -f build
506 # Check recipe to run (extra) defined functions by the packager
507 for _definition in \
508 $(awk '!/^build[ (]/ && /^[^ {}]+ *\(\)/{ gsub(/[()]/, "", $1); print $1 }' "${CWD}/$recipe")
510 # Check if it is a shell function
511 case $(LC_ALL=C type $_definition) in
512 *function*)
513 # Call and undo the function after executing it
514 $_definition
515 unset -f $_definition
517 esac
518 done
519 unset -v _definition
521 # Turn off possible shell flags coming from the recipe
523 set +e
524 if test "${_xtrace_flag:+$_xtrace_flag}" != _xtrace_flag_is_set
525 then
526 set +x
529 # Reset given signals
530 trap - EXIT HUP INT QUIT ABRT TERM
532 # If 'destdir' is empty, the package won't be created
533 if rmdir -- "$destdir" 2> /dev/null
534 then
535 warn "The package \"${full_pkgname}.tlz\" won't be created. 'destdir' is empty."
536 opt_nopkg=opt_nopkg
539 # Create (make) the package
541 if test "$opt_nopkg" != opt_nopkg
542 then
543 # Edit the recipe when 'release' is incremented
544 if test "$opt_incr_release" = opt_incr_release
545 then
546 echo ",s/^\\(release\\)=.*/\\1=${release}/"$'\nw' | \
547 ed "${CWD}/$recipe" || chkstatus_or_exit
550 mkdir -p -- "${destdir}/var/lib/qi" || chkstatus_or_exit
552 # Include a copy of the recipe into the package
553 cp -p "${CWD}/$recipe" \
554 "${destdir}/var/lib/qi/${full_pkgname}.recipe" && \
555 chmod 644 "${destdir}/var/lib/qi/${full_pkgname}.recipe"
556 chkstatus_or_exit
558 # Detect post-install script for inclusion
560 if test -f "${CWD}/post-install"
561 then
562 echo "${CWD}/post-install: Detected."
563 cp -p "${CWD}/post-install" \
564 "${destdir}/var/lib/qi/${full_pkgname}.sh" && \
565 chmod 644 "${destdir}/var/lib/qi/${full_pkgname}.sh"
566 chkstatus_or_exit
569 # Detect declared package name(s) for later replacement
571 if test -n "$replace"
572 then
573 warn \
574 "=== The following package names has been declared for replacement:" \
575 "$replace"
577 : > "${destdir}/var/lib/qi/${full_pkgname}.replace"
578 for item in $replace
580 printf '%s\n' "$replace" >> \
581 "${destdir}/var/lib/qi/${full_pkgname}.replace"
582 done
583 unset -v item
586 # Create (external) meta file for package information,
587 # make a copy of it for the package database
588 echo "=== Creating meta file ${full_pkgname}.tlz.txt ..."
589 do_meta > "${outdir}/${full_pkgname}.tlz.txt" || chkstatus_or_exit
590 cp -p "${outdir}/${full_pkgname}.tlz.txt" \
591 "${destdir}/var/lib/qi/${full_pkgname}.txt" || chkstatus_or_exit
593 # Produce the package
594 cd -- "$destdir" && mode_create "${outdir}/${full_pkgname}.tlz"
597 # Back to the current working directory
598 cd -- "$CWD" || chkstatus_or_exit
600 echo "=== Deleting 'srcdir', 'destdir' ..."
601 if test "$opt_keep" != opt_keep
602 then
603 if test -z "$keep_srcdir"
604 then
605 srcdir="${srcdir%%/*}"; # Directory name without parents.
606 if test -e "${TMPDIR}/$srcdir"
607 then
608 rm -r -- "${TMPDIR:?}/$srcdir" || chkstatus_or_exit
609 echo "removed directory: '${TMPDIR}/$srcdir'"
613 if test -z "$keep_destdir"
614 then
615 if test -e "$destdir"
616 then
617 rm -r -- "$destdir" || chkstatus_or_exit
618 echo "removed directory: '$destdir'"
621 else
622 warn \
623 " Request via --keep to preserve 'srcdir' or 'destdir' has been made:" \
624 "${TMPDIR}/$srcdir" \
625 "$destdir"
628 # Install or upgrade the package after build
629 if test "$opt_nopkg" != opt_nopkg
630 then
631 if test "$opt_install" = opt_install
632 then
633 mode_install "${outdir}/${full_pkgname}.tlz"
634 elif test "$opt_upgrade" = opt_upgrade
635 then
636 mode_upgrade "${outdir}/${full_pkgname}.tlz"
640 echo "{@} All done for \"${CWD}/${recipe}\"."
641 echo ""
644 mode_create()
646 directory="$(dirname -- "$1")"
648 # Perform sanity checks
650 if ! fnmatch '/?*' "$directory"
651 then
652 warn "${PROGRAM}: Output directory \`${directory}' is not fully qualified"
653 exit 4
655 is_readable "$directory" || exit 4
657 name="$(basename -- "$1")"
659 echo ""
660 echo "{#} Creating package name \`${name}' ..."
662 if test "$name" = "${name%.tlz}"
663 then
664 warn "Package format '$name' not supported." \
665 "It should be \"name_version_architecture-release[@pkgcategory].tlz\""
666 exit 4
669 # Pass extra options for tarlz(1)
670 if test -n "$SOURCE_DATE_EPOCH"
671 then
672 tarlz_opts="--mtime=@${SOURCE_DATE_EPOCH}"
675 ( umask 022 ; tarlz --solid -9 $tarlz_opts -cvf - -- * ) \
676 > "${directory}/$name"
677 chkstatus_or_exit 3
678 unset -v tarlz_opts
680 ( cd -- "$directory" && sha256sum "$name" > "${name}.sha256" )
681 chkstatus_or_exit 4
683 echo "{#} Package \"${name}\" created on ${directory}."
685 # Remove used variables
686 unset -v directory name
688 echo ""
691 mode_remove()
693 expunge="${packagedir}/$(basename -- "$1" .tlz)"
695 echo ""
696 echo "<<< Removing package \`$rootdir${expunge}' ..."
698 # Complain if the package directory does not exist
700 test -e "$rootdir${expunge}" || {
701 warn "Package directory '$rootdir${expunge}' does not exist."
702 return 4
705 # Complain if the package directory cannot be well-read
707 is_readable "$rootdir${expunge}" || exit 4
709 # Remove package from Graft control
711 # Scan for possible conflicts, stop if arise
712 if test "$opt_prune" != opt_prune
713 then
714 echo "=== Checking for possible conflicts ..."
715 if graft -d -n $graft_r -t "$targetdir" "$expunge" 2>&1 | \
716 grep "^CONFLICT"
717 then
718 warn "" \
719 " A conflict occurred during uninstallation;" \
720 "Unless the --prune option is given, this package will be PRESERVED."
721 return 6;
725 # Remove objects (files, links or directories) from the target
726 # directory that are in conflict with the package directory
728 echo "=== Pruning any conflict ..."
729 graft -p -D -u $graft_r -t "$targetdir" "$expunge"
730 chkstatus_or_exit 2
732 echo "=== Disabling links ..."
733 graft -d -D -u $graft_v $graft_r -t "$targetdir" "$expunge"
734 chkstatus_or_exit 2
736 # Delete package directory
737 if test "$opt_keep" != opt_keep
738 then
739 echo "=== Deleting package directory, if exists as such ..."
740 if test -e "${rootdir}$expunge"
741 then
742 rm -r -- "${rootdir}$expunge" || chkstatus_or_exit
743 echo "removed directory: '${rootdir}$expunge'"
747 # Remove used variables
748 unset -v expunge
750 echo ""
753 mode_install()
755 # Complain if the package cannot be well-read
757 is_readable "$1" || exit 4
759 # Complain if the package does not end in .tlz
761 if ! fnmatch '*.tlz' "$1"
762 then
763 warn "\`${1}' does not end in .tlz"
764 return 4
767 # Make preparations to install the package
769 # Get the filename
770 name="$(basename -- "$1" .tlz)"
772 echo ""
773 echo ">>> Installing package \`${name}' ..."
775 echo "=== Checking tarball integrity ..."
776 tarlz --missing-crc -tf "$1" > /dev/null
777 chkstatus_or_exit 3
779 # To accept random directory from the upgrade mode
780 _packagedir="$2"
781 _packagedir="${_packagedir:=$packagedir}"
783 # Create package directory using 'name'
784 if ! test -d "$rootdir${_packagedir}/$name"
785 then
786 mkdir -p -- "$rootdir${_packagedir}/$name" || chkstatus_or_exit
787 echo "mkdir: created directory '$rootdir${_packagedir}/$name'"
790 # Scan for possible conflicts, stop if arise
791 if test "$opt_prune" != opt_prune
792 then
793 echo "=== Checking for possible conflicts ..."
794 if graft -i -n $graft_r -t "$targetdir" "${_packagedir}/$name" 2>&1 | \
795 grep "^CONFLICT"
796 then
797 warn "" \
798 " A conflict occurred during installation;" \
799 "Unless the --prune option is given, this package won't be LINKED."
800 return 6;
804 echo "=== Decompressing package ..."
805 ( cd -- "$rootdir${_packagedir}/$name" && tarlz -xpf - ) < "$1"
806 chkstatus_or_exit 3
808 # Transite package to Graft control
810 # Remove objects (files, links or directories) from the target
811 # directory that are in conflict with the package directory
812 echo "=== Pruning any conflict ..."
813 graft -p -D -u $graft_r -t "$targetdir" "${_packagedir}/$name"
814 chkstatus_or_exit 2
816 echo "=== Enabling symbolic links ..."
817 graft -i -P $graft_v $graft_r -t "$targetdir" "${_packagedir}/$name"
818 chkstatus_or_exit 2
820 # Avoid unnecessary runs coming from the upgrade mode,
821 # this is when the incoming package is **pre-installed**
823 if test "$_isUpgrade" != _isUpgrade.on
824 then
825 # Show package description
826 if test -r "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.txt"
827 then
828 awk '/^#/' "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.txt"
829 elif test -r "${1}.txt"
830 then
831 # From external meta file (current directory)
832 awk '/^#/' "${1}.txt"
833 else
834 warn "Description file not found for '$name'."
837 # Check and run the post-install script if exist
838 if test -r "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.sh"
839 then
840 echo "=== Running post-install script for \`${name}' ..."
842 # Rely on 'targetdir' if 'rootdir' is empty
843 cd -- "${rootdir:=$targetdir}"/ && \
844 . "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.sh"
848 # Check if there are declared packages for replacement
849 if test -r "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.replace"
850 then
851 while read -r line
853 for replace in "$rootdir${_packagedir}/${line%%_*}"_*
855 if ! test -e "$replace"
856 then
857 warn "<^> Declared package \`${replace}' does not exist. (ignoring)"
858 continue;
861 replace="${replace##*/}"
863 # The search for the package to be replaced cannot
864 # be the same to the incoming package, even to the
865 # temporary location coming from the upgrade mode
866 if test "$replace" = "$name" || \
867 test "_x_${replace}" = "_x_${PRVLOC##*/}"
868 then
869 continue;
872 warn "WARNING: Replacing package \`${replace}' ..."
874 # Since the links belongs to the new package, only
875 # those which are not in conflict can be deleted.
876 # To complete, we will remove the package directory
878 graft -d -D -u $graft_r \
879 -t "$targetdir" "$replace" > /dev/null 2>&1
881 rm -rf -- "$rootdir${_packagedir}/$replace"
882 done
883 done < "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.replace"
887 # Remove used variables
888 unset -v name _packagedir
890 echo ""
893 mode_order()
895 # Complain if the file cannot be well-read
897 is_readable "$1" || exit 4
899 # Complain if the file does not end in .order
901 if ! fnmatch '*.order' "$1"
902 then
903 warn "\`${1}' does not end in .order"
904 return 4
907 # Get a clean list of the file while printing its contents in
908 # reverse order. Last awk(1) in the pipeline eliminates
909 # non-consecutive lines: duplicates, blank lines, and colons.
910 # Comment lines beginning with '#' are allowed; parentheses
911 # are simply ignored
913 awk '{
914 gsub( /:|^#(.*)$|\([^)]*)|^$/,"" );
915 for( i=NF; i > 0; i-- ) print $i
916 }' "$1" | awk '!s[$0]++'
919 mode_upgrade()
921 # Complain if the package does not end in .tlz
923 if ! fnmatch '*.tlz' "$1"
924 then
925 warn "\`${1}' does not end in .tlz"
926 return 4
929 # Get the filename
930 incoming="$(basename -- "$1" .tlz)"
932 echo ""
933 echo "{%} Upgrading package \`${incoming}' ..."
935 # Check package pre-existence
936 if test "$opt_force" != opt_force && \
937 test -e "$rootdir${packagedir}/$incoming"
938 then
939 warn \
940 "" \
941 " The package to be upgraded already exist;" \
942 "Unless the --force option is given, this package won't be UPGRADED."
943 return 6;
946 # Ignore some signals until the upgrade process is complete
947 trap "" HUP INT QUIT ABRT TERM
949 # Check blacklisted packages before to proceed with the upgrade
951 echo "=== Checking blacklist ..."
952 for item in $blacklist
954 case $item in
955 "${incoming%@*}"*)
956 warn \
957 "A BLACKLISTED package name has been detected:" \
958 "-> $incoming" \
959 "" \
960 " This package will be INSTALLED instead of being upgraded."
961 opt_prune=opt_prune mode_install "$1"
962 return 0
964 esac
965 done
966 unset -v item
968 # Prepare the package to install it in a temporary but custom location
970 # Set random directory under 'rootdir/packagedir' using 'incoming' as name
971 PRVLOC=$(mktemp -dp "$rootdir${packagedir}" ${incoming}.XXXXXXXXXXXX) || exit 2
973 echo "=== Pre-installing package in temporary location ..."
974 opt_prune=opt_prune # Turn on prune operation.
975 _isUpgrade=_isUpgrade.on mode_install "$1" "$PRVLOC" > /dev/null
976 _isUpgrade=_isUpgrade.off
978 echo "=== Looking for installations under the same name ..."
979 for long_name in "$rootdir${packagedir}/${incoming%%_*}"_*
981 # Check if it is a valid directory and if the incoming
982 # package is not equivalent to a previous installation;
983 # also check if the package to be deleted is equivalent
984 # to the temporary, private location.
986 if ! test -d "$long_name"
987 then
988 continue;
990 if test "${long_name##*/}" = "$incoming"
991 then
992 continue;
994 if test "$long_name" = "$PRVLOC"
995 then
996 continue;
999 warn "Proceeding to the removal of ${long_name} ..."
1000 # A package directory will be preserved if --keep is given
1001 mode_remove "${long_name##*/}" > /dev/null
1002 done
1003 unset -v long_name
1005 # Re-install the package removing the temporary location
1007 mode_install "$1"
1008 opt_prune=opt_prune.off # Turn off prune operation.
1010 echo "=== Deleting temporary location ..."
1011 rm -rf -- "$PRVLOC" || chkstatus_or_exit
1012 echo "removed directory: '$PRVLOC'"
1014 echo "{%} Upgraded from \"${1}\"."
1016 # Remove remaining variables
1017 unset -v incoming PRVLOC
1019 echo ""
1021 # Reset given signals
1022 trap - HUP INT QUIT ABRT TERM
1025 mode_warn()
1027 # Complain if the package cannot be well-read
1029 is_readable "$1" || exit 4
1031 # Complain if the package does not end in .tlz
1033 if ! fnmatch '*.tlz' "$1"
1034 then
1035 warn "\`${1}' does not end in .tlz"
1036 return 4
1039 # List content of files excluding directories
1040 while test -f "$1"
1042 tarlz -tvvf "$1" | awk '!/^drwx/'
1043 chkstatus_or_exit 3
1044 shift;
1045 done
1046 return 0
1049 mode_extract()
1051 # Perform sanity checks before package extraction
1053 is_readable "$1" || exit 4
1055 test -f "$1" || {
1056 warn "\`${1}' is not a regular file."
1057 return 4
1060 # Preparations to extract the package
1062 name="$(basename -- "$1" .tlz)"
1064 # Set random directory under 'TMPDIR' using 'name'
1065 PRVDIR=$(mktemp -dp "$TMPDIR" ${name}.XXXXXXXXXXXX) || exit 2
1067 # Trap to remove 'PRVDIR' on disruptions
1068 trap 'rm -rf -- "$PRVDIR"' HUP INT ABRT TERM
1070 # Create 'PRVDIR' removing access for all but user
1071 ( umask 077 ; mkdir -- "$PRVDIR" )
1072 mkdir -p -- "$PRVDIR"
1073 chmod 700 -- "$PRVDIR"
1075 echo ""
1076 echo "--- Extracting package \`${name}' ..."
1078 ( umask 000 ; cd -- "$PRVDIR" && tarlz -xvf - ) < "$1"
1079 if test $? -ne 0
1080 then
1081 # Try to remove (empty) 'PRVDIR' on failure
1082 rmdir -- "$PRVDIR"
1083 exit 3;
1086 echo ""
1087 echo "\"${name}\" has been extracted on \`${PRVDIR}'"
1089 # Reset given signals
1090 trap - HUP INT ABRT TERM
1092 # Remove used variables
1093 unset -v name PRVDIR
1096 ### Extra functions to be used during the modes
1098 unpack()
1100 for file in "$@"
1102 case $file in
1103 *.tar)
1104 tar -tf "$file" > /dev/null && \
1105 tar -xpf "$file"
1106 chkstatus_or_exit 3
1108 *.tar.gz | *.tgz | *.tar.Z )
1109 gzip -cd "$file" | tar -tf - > /dev/null && \
1110 gzip -cd "$file" | tar -xpf -
1111 chkstatus_or_exit 3
1113 *.tar.bz2 | *.tbz2 | *.tbz )
1114 bzip2 -cd "$file" | tar -tf - > /dev/null && \
1115 bzip2 -cd "$file" | tar -xpf -
1116 chkstatus_or_exit 3
1118 *.tar.lz | *.tlz )
1119 lzip -cd "$file" | tar -tf - > /dev/null && \
1120 lzip -cd "$file" | tar -xpf -
1121 chkstatus_or_exit 3
1123 *.tar.xz | *.txz )
1124 xz -cd "$file" | tar -tf - > /dev/null && \
1125 xz -cd "$file" | tar -xpf -
1126 chkstatus_or_exit 3
1128 *.zip | *.ZIP )
1129 unzip -t "$file" > /dev/null && \
1130 unzip "$file" > /dev/null
1131 chkstatus_or_exit 3
1133 *.gz)
1134 gzip -t "$file" && \
1135 gzip -cd "$file" > "$(basename -- "$file" .gz)"
1136 chkstatus_or_exit 3
1138 *.Z)
1139 gzip -t "$file" && \
1140 gzip -cd "$file" > "$(basename -- "$file" .Z)"
1141 chkstatus_or_exit 3
1143 *.bz2)
1144 bzip2 -t "$file" && \
1145 bzip2 -cd "$file" > "$(basename -- "$file" .bz2)"
1146 chkstatus_or_exit 3
1148 *.lz)
1149 lzip -t "$file" && \
1150 lzip -cd "$file" > "$(basename -- "$file" .lz)"
1151 chkstatus_or_exit 3
1153 *.xz)
1154 xz -t "$file" && \
1155 xz -cd "$file" > "$(basename -- "$file" .xz)"
1156 chkstatus_or_exit 3
1159 warn "${PROGRAM}: cannot unpack ${file}: Unsupported extension"
1160 exit 1
1161 esac
1162 done
1163 unset -v file
1166 do_meta()
1168 # Extract information from the recipe to create the meta file.
1170 # The package description is pre-formatted in 78 columns,
1171 # the '#' character and a space is added as prefix to conform
1172 # the 80 columns in total
1173 printf '%s\n' "$description" | fold -w 78 | awk '$0="# " $0'
1175 # Include build flags only if it is a real architecture
1176 if test "$arch" != noarch
1177 then
1178 printf '%s' \
1180 QICFLAGS=\"$QICFLAGS\"
1181 QICXXFLAGS=\"$QICXXFLAGS\"
1182 QILDFLAGS=\"$QILDFLAGS\"
1183 QICPPFLAGS=\"$QICPPFLAGS\""
1186 # Print saving the rest of the package information
1187 printf '%s' \
1189 pkgname=$pkgname
1190 pkgversion=$pkgversion
1191 arch=$arch
1192 release=$release
1193 pkgcategory=\"${pkgcategory#@*}\"
1194 full_pkgname=$full_pkgname
1195 blurb=\"$(printf '%s\n' "$description" | sed -e '/^$/d;2q')\"
1196 homepage=\"$homepage\"
1197 license=\"$license\"
1198 fetch=\"$fetch\"
1199 replace=\"$replace\"
1203 ### Default values
1205 packagedir=@PACKAGEDIR@
1206 targetdir=@TARGETDIR@
1207 blacklist="perl5 graft tarlz plzip musl glibc coreutils bash mksh"
1208 _rcfile=@SYSCONFDIR@/qirc
1209 opt_install=opt_install.off
1210 opt_upgrade=opt_upgrade.off
1211 opt_force=opt_force.off
1212 opt_keep=opt_keep.off
1213 opt_incr_release=opt_incr_release.off
1214 opt_skipqsts=opt_skipqsts.off
1215 opt_nopkg=opt_nopkg.off
1216 opt_prune=opt_prune.off
1217 verbose=0
1218 rootdir=""
1219 arch=@ARCH@
1220 jobs=1
1221 mode=""
1222 _xtrace_flag=""
1223 _readstdin=""
1224 graft_v=""
1225 graft_r=""
1226 _isUpgrade=_isUpgrade.off
1227 keep_srcdir=""
1228 keep_destdir=""
1229 TMPDIR="${TMPDIR:-/usr/src/qi/build}"
1230 QICFLAGS="${QICFLAGS:--O2}"
1231 QICXXFLAGS="${QICXXFLAGS:--O2}"
1232 QILDFLAGS="${QILDFLAGS:-}"
1233 QICPPFLAGS="${QICPPFLAGS:-}"
1234 worktree=/usr/src/qi
1235 tardir=${worktree}/sources
1236 outdir=/var/cache/qi/packages
1237 netget="wget -c -w1 -t3 --no-check-certificate"
1238 rsync="rsync -v -a -L -z -i --progress"
1239 configure_args="--prefix=@PREFIX@ --libexecdir=@LIBEXECDIR@ --bindir=@BINDIR@ --sbindir=@SBINDIR@ --sysconfdir=@SYSCONFDIR@ --localstatedir=@LOCALSTATEDIR@"
1240 infodir=@INFODIR@
1241 mandir=@MANDIR@
1242 docdir=@DOCDIR@
1244 # Store (default) directory locations
1245 QI_TARGETDIR=$targetdir
1246 QI_PACKAGEDIR=$packagedir
1247 QI_WORKTREE=$worktree
1248 QI_TARDIR=$tardir
1249 QI_OUTDIR=$outdir
1251 ### Parse commands and options
1253 validate_mode()
1255 if test -n "$mode"
1256 then
1257 warn "${PROGRAM}: First defined command: ${mode#*_}" \
1258 "Switching to another command is not allowed (${1})."
1259 exit 1
1263 validate_option()
1265 if test -z "$2"
1266 then
1267 warn "${PROGRAM}: The '${1}' option requires an argument" \
1268 "Try '${PROGRAM} --help' for more information."
1269 exit 1
1273 validate_directory()
1275 if test ! -d "$2"
1276 then
1277 warn "${PROGRAM}: ${1} \"${2}\" must be a valid directory name"
1278 exit 1
1282 validate_digit()
1284 name="$1"
1285 shift
1287 # Taken from https://mywiki.wooledge.org/BashFAQ/054
1288 case "${1#[-+]}" in
1290 warn "${PROGRAM}: The '${name}' option has no defined value"
1291 exit 1
1293 *.*.*)
1294 warn "${PROGRAM}: The '${name}' option has more than one decimal point on it \"${1}\""
1295 exit 1
1297 *[!0-9]*)
1298 warn "${PROGRAM}: The '${name}' option contains a non-valid digit on it \"${1}\""
1299 exit 1
1301 esac
1303 unset -v name
1306 while test $# -gt 0
1308 case $1 in
1309 warn)
1310 validate_mode warn
1311 mode=mode_warn
1313 install)
1314 validate_mode install
1315 readconfig
1316 mode=mode_install
1318 remove)
1319 validate_mode remove
1320 readconfig
1321 mode=mode_remove
1323 upgrade)
1324 validate_mode upgrade
1325 readconfig
1326 mode=mode_upgrade
1328 extract)
1329 validate_mode extract
1330 readconfig
1331 mode=mode_extract
1333 create)
1334 validate_mode create
1335 readconfig
1336 mode=mode_create
1338 order)
1339 validate_mode order
1340 readconfig
1341 mode=mode_order
1343 build)
1344 validate_mode build
1345 readconfig
1346 mode=mode_build
1348 --no-rc | -N )
1349 _readconfig=readconfig.off
1351 --install | -i )
1352 opt_install=opt_install
1354 --upgrade | -u )
1355 opt_upgrade=opt_upgrade
1357 --force | -f )
1358 opt_force=opt_force
1360 --keep | -k )
1361 opt_keep=opt_keep
1363 --prune | -p )
1364 opt_prune=opt_prune
1366 --packagedir | -P )
1367 validate_option "$1" "$2"
1368 packagedir="$2"
1369 validate_directory "$1" "$packagedir"
1370 shift
1372 --packagedir=*)
1373 validate_option "$1" "$2"
1374 packagedir="${1#*=}"
1375 validate_directory "$1" "$packagedir"
1377 --targetdir | -t )
1378 validate_option "$1" "$2"
1379 targetdir="$2"
1380 validate_directory "$1" "$targetdir"
1381 shift
1383 --targetdir=*)
1384 validate_option "$1" "$2"
1385 targetdir="${1#*=}"
1386 validate_directory "$1" "$targetdir"
1388 --rootdir | -r )
1389 validate_option "$1" "$2"
1390 rootdir="$2"
1391 validate_directory "$1" "$rootdir"
1392 shift
1394 --rootdir=*)
1395 validate_option "$1" "$2"
1396 rootdir="${1#*=}"
1397 validate_directory "$1" "$rootdir"
1399 --outdir | -o )
1400 validate_option "$1" "$2"
1401 outdir="$2"
1402 validate_directory "$1" "$outdir"
1403 shift
1405 --outdir=*)
1406 validate_option "$1" "$2"
1407 outdir="${1#*=}"
1408 validate_directory "$1" "$outdir"
1410 --worktree | -w )
1411 validate_option "$1" "$2"
1412 worktree="$2"
1413 validate_directory "$1" "$worktree"
1414 shift
1416 --worktree=*)
1417 validate_option "$1" "$2"
1418 worktree="${1#*=}"
1419 validate_directory "$1" "$worktree"
1421 --sourcedir | -s )
1422 validate_option "$1" "$2"
1423 tardir="$2"
1424 validate_directory "$1" "$tardir"
1425 shift
1427 --sourcedir=*)
1428 validate_option "$1" "$2"
1429 tardir="${1#*=}"
1430 validate_directory "$1" "$tardir"
1432 --architecture | -a )
1433 validate_option "$1" "$2"
1434 arch="$2"
1435 shift
1437 --arch=*)
1438 validate_option "$1" "$2"
1439 arch="${1#*=}"
1441 --jobs | -j )
1442 jobs="$2"
1443 validate_digit "$1" "$jobs"
1444 shift
1446 -j[0-9]*)
1447 jobs="${1#-j*}"
1448 validate_digit '-j' "$jobs"
1450 --jobs=*)
1451 jobs="${1#*=}"
1452 validate_digit '--jobs=' "$jobs"
1454 --no-package | -n )
1455 opt_nopkg=opt_nopkg
1457 --increment | -1 )
1458 opt_incr_release=opt_incr_release
1460 --skip-questions | -S )
1461 opt_skipqsts=opt_skipqsts
1463 --verbose | -v )
1464 verbose=$(( verbose + 1 ))
1466 -vv)
1467 # ^ Trick for a second -v.
1468 verbose=2
1470 --show-location | -L )
1471 printf '%s\n' \
1472 "QI_TARGETDIR=$QI_TARGETDIR" \
1473 "QI_PACKAGEDIR=$QI_PACKAGEDIR" \
1474 "QI_WORKTREE=$QI_WORKTREE" \
1475 "QI_TARDIR=$QI_TARDIR" \
1476 "QI_OUTDIR=$QI_OUTDIR"
1477 exit 0
1479 --help | --hel | --he | --h | '--?' | -help | -hel | -he | -h | '-?' | \
1480 help )
1481 usage
1482 exit 0
1484 --version | --versio | --versi | --vers | \
1485 -version | -versio | -versi | -vers | -V | version )
1486 echo "$PROGRAM version @VERSION@"
1487 exit 0
1489 '-')
1490 _readstdin=readstdin
1491 break
1494 shift
1495 break; # End of options.
1498 warn "qi: Unrecognized option: $1" \
1499 "Try '${PROGRAM} --help' for more information."
1500 exit 1
1503 break; # No more options.
1505 esac
1506 shift
1507 done
1508 unset -f \
1509 readconfig validate_mode validate_option validate_directory validate_digit
1511 # When there are no arguments, show the help
1512 if test $# -eq 0
1513 then
1514 usage
1515 exit 1
1517 unset -f usage
1519 # Program sanity check
1521 for need in awk basename chmod cp dirname find fold graft grep \
1522 mkdir mktemp rm rmdir sed sha256sum stat tarlz ; \
1524 if ! \command -v $need > /dev/null
1525 then
1526 warn "${PROGRAM}: Prerequisite \`${need}' not found in PATH"
1527 exit 2
1529 done
1530 unset -v need
1532 # Set verbosity level/flag
1534 if test "$verbose" -gt 0
1535 then
1536 if test "$verbose" -eq 1
1537 then
1538 graft_v=-v
1539 else
1540 graft_v=-V
1544 # Read standard input if FILE is -, or when
1545 # FILE is not connected to a terminal
1547 if test "$_readstdin" = readstdin
1548 then
1549 if test -t 0
1550 then
1551 warn "qi: I won't read from a connected terminal." \
1552 "Try '${PROGRAM} --help' for more information."
1553 exit 1
1556 # Unset positional parameters setting $# to zero
1557 set --
1559 # Assign remaining arguments to the positional parameters
1560 while read -r input
1562 set -- "$@" "$input"
1563 done
1565 unset -v _readstdin
1567 if test -z "$mode"
1568 then
1569 warn "qi: We need at least one (valid) command." \
1570 "Try '${PROGRAM} --help' for more information."
1571 exit 4
1574 # Validate 'packagedir' and 'targetdir' as canonical directories
1576 # The single slash '/' does not qualify here
1577 if ! fnmatch '/?*' "$packagedir"
1578 then
1579 warn "${PROGRAM}: Package directory \`${packagedir}' is not fully qualified"
1580 exit 4
1582 if test ! -d "$packagedir"
1583 then
1584 warn "${PROGRAM}: Package directory \`${packagedir}' does not exist"
1585 exit 4
1588 # The single slash '/' is valid here
1589 if ! fnmatch '/*' "$targetdir"
1590 then
1591 warn "${PROGRAM}: Target directory \`${targetdir}' is not fully qualified"
1592 exit 4
1594 if test ! -d "$targetdir"
1595 then
1596 warn "${PROGRAM}: Target directory \`${targetdir}' does not exist"
1597 exit 4
1600 # Validate 'rootdir' directory
1602 if test -n "$rootdir"
1603 then
1604 if test -d "$rootdir" && test "$rootdir" != /
1605 then
1606 rootdir="${rootdir%/}" # Remove slash from the end.
1608 # A workaround for graft-2.13+. The specified directory is
1609 # relative to the log file, we prepend it inside 'rootdir'
1611 eval "$(graft -L)" ; GRAFT_LOGFILE="${GRAFT_LOGFILE:=/var/log/graft}"
1612 mkdir -p -- "$rootdir$(dirname -- "$GRAFT_LOGFILE")" || chkstatus_or_exit
1614 # Compose 'rootdir' and log file option to be used with graft(1)
1615 graft_r="-r $rootdir -l $GRAFT_LOGFILE"
1617 # Unset variables coming from eval
1618 unset -v GRAFT_PERL GRAFT_LOGFILE GRAFT_TARGETDIR GRAFT_PACKAGEDIR
1619 else
1620 warn "${PROGRAM}: Root directory \`${rootdir}' is not fully qualified"
1621 exit 4
1623 readonly rootdir
1624 export rootdir
1627 # Ensure 'TMPDIR' creation to prefix temporary files
1629 if test ! -d "$TMPDIR"
1630 then
1631 mkdir -p -- "$TMPDIR" || chkstatus_or_exit
1633 readonly TMPDIR packagedir targetdir
1635 # Process each package or recipe provided on the command-line
1637 for package in "$@"
1639 $mode "$package"
1640 done