stages: 2/04-iso: Do not include kernel build/source tree in the LiveCD
[dragora.git] / qi / src / qi.in
blob0ef31a4b0b02ec8239143e725630ba79e2b1b3c2
1 #! /bin/sh -
2 # Copyright (C) 2016-2024 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 echo "${PROGRAM}: cannot access ${1}: No such file or directory" 1>&2
113 return 1
115 if test ! -r "$1"
116 then
117 echo "${PROGRAM}: cannot read ${1}: Permission denied" 1>&2
118 return 1
120 return 0
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 is_readable "$HOME/.qirc" 2> /dev/null
162 then
163 _rcfile="$HOME/.qirc"
166 echo "Importing configuration file from \`${_rcfile}' ..."
167 . "$_rcfile" || chkstatus_or_exit 5
170 validate_file()
172 # Complain if the file cannot be well-read
173 is_readable "$1" || exit 4
175 # Complain if the file does not end at expected extension
176 if ! fnmatch "*.${2}" "$1"
177 then
178 echo "${PROGRAM}: \`${1}' does not end in .${2}" 1>&2
179 exit 4
182 # Complain if the file is not regular
183 if test ! -f "$1"
184 then
185 echo "${PROGRAM}: \`${1}' is not a regular file." 1>&2
186 exit 4
190 make_directory()
192 if test ! -d "$1"
193 then
194 ( umask 022 ; mkdir -p -- "$1" || exit $? ); chkstatus_or_exit
198 ### Mode functions
200 mode_build()
202 # Read configuration file if --no-rc is not passed
203 test "$opt_readrc" = opt_readrc && readconfig
205 recipe=$1
207 echo ""
208 echo "{@} Building \"${recipe}\" ..."
210 # A recipe is any valid regular file. Qi sets priorities for reading
211 # a recipe, the order in which qi looks for a recipe is:
213 # 1. Current working directory.
215 # 2. If the specified path name does not contain "recipe" as the last
216 # component. Qi will complete it by adding "recipe" to the path
217 # name.
219 # 3. If the recipe is not in the current working directory, it will be
220 # searched under '${worktree}/recipes'. The last component will be
221 # completed adding "recipe" to the specified path name.
223 if test ! -f "$recipe"
224 then
225 if test -e "${recipe}/recipe"
226 then
227 recipe="${recipe}/recipe"
228 elif test -e "${worktree}/recipes/${recipe}/recipe"
229 then
230 recipe="${worktree}/recipes/${recipe}/recipe"
234 test -f "$recipe" || {
235 warn "\`${recipe}' is not a regular file."
236 exit 4
239 # Complain if the file name is not "recipe"
241 if test "${recipe##*/}" != recipe
242 then
243 warn "\`${recipe}' is not a valid recipe name."
244 exit 4
247 # Start preparations to import the recipe
249 # Get working directory and base name of the recipe
251 CWD="$(CDPATH='' cd -P -- "$(dirname -- "$recipe")" && pwd -P)"
252 recipe="$(basename -- "$recipe")"
254 # Check readability for load the recipe on success
256 is_readable "${CWD}/$recipe" || exit 4
258 # Create external directories if needed
259 for directory in "${worktree}/archive" "${worktree}/patches" "${worktree}/recipes" "$tardir"
261 make_directory "$directory"
262 done
263 unset -v directory
265 # Variables treatment for the current and the next recipe.
267 # Unset special variables that can only be predefined in
268 # the recipe and does not come from '${sysconfdir}/qirc'
270 unset -v \
271 srcdir destdir pkgname pkgversion pkgcategory program version release \
272 fetch description homepage license replace full_pkgname docs docsdir \
273 CFLAGS CXXFLAGS LDFLAGS CPPFLAGS
275 # The following variables must be restored, later
276 save_arch="${save_arch:=$arch}"
277 save_jobs="${save_jobs:=$jobs}"
278 save_outdir="${save_outdir:=$outdir}"
279 save_opt_nopkg="${save_opt_nopkg:=$opt_nopkg}"
280 save_tarlz_compression_options="${save_tarlz_compression_options:=$tarlz_compression_options}"
282 # Reset variable values in case of return
283 arch="$save_arch"
284 jobs="$save_jobs"
285 outdir="$save_outdir"
286 opt_nopkg="$save_opt_nopkg"
287 tarlz_compression_options="$save_tarlz_compression_options"
289 # The following variables cannot be redefined on a recipe
290 readonly worktree netget rsync
292 # Import the recipe
294 . "${CWD}/$recipe"
295 chkstatus_or_exit
297 # Check if 'opt_skiprecipe' has been declared to ignore
298 # the current recipe and continue with the next recipe
299 if test "$opt_skiprecipe" = "opt_skiprecipe"
300 then
301 unset -v opt_skiprecipe
302 warn "${recipe}: The variable 'opt_skiprecipe' has been used here."
303 return 0;
306 # Check for required variables
307 if test -z "$program"
308 then
309 warn "${recipe}: The variable 'program' is not defined."
310 exit 5
312 if test -z "$version"
313 then
314 warn "${recipe}: The variable 'version' is not defined."
315 exit 5
317 if test -z "$arch"
318 then
319 warn "${recipe}: The variable 'arch' is not defined."
320 exit 5
322 if test -z "$release"
323 then
324 warn "${recipe}: The variable 'release' is not defined."
325 exit 5
328 # Pre-settings before to start building
330 # Increment the release number if the option was given
331 if test "$opt_incr_release" = opt_incr_release
332 then
333 release=$(( release + 1 ))
336 # Allow the dot as definition for 'tardir'
337 if test "$tardir" = .
338 then
339 tardir="$CWD"
342 # Set default values for the following special variables
344 pkgname="${pkgname:=$program}"
345 pkgversion="${pkgversion:=$version}"
346 srcdir="${srcdir:=${program}-$version}"
347 srcdir="${TMPDIR}/$srcdir"
348 destdir="${destdir:=${TMPDIR}/package-$pkgname}"
350 # If 'pkgcategory' has been defined, prefix it using the "at" symbol
351 if test -n "$pkgcategory"
352 then
353 pkgcategory="@${pkgcategory}"
356 # Compose the full package name
357 full_pkgname="${full_pkgname:=${pkgname}_${pkgversion}_${arch}-${release}${pkgcategory}}"
359 # Use 'arch' as suffix for 'outdir' to have a well-organized package output
360 outdir="${outdir}/${arch}"
362 # If a package is going to be created the existence of a
363 # previous build will be detected and reported. Under normal
364 # conditions the recipe is built as long as it is newer than
365 # the produced package, if not, we warn to the user about it.
366 # Rebuilding the package is possible (through the force ;-)
368 if test "$opt_nopkg" != opt_nopkg && \
369 { test "$opt_force" != opt_force && \
370 test -e "${outdir}/${full_pkgname}.tlz" ; }
371 then
372 if is_newer "${CWD}/$recipe" "${outdir}/${full_pkgname}.tlz"
373 then
374 warn \
375 "" \
376 "This recipe is more RECENT than the produced package:" \
377 "" \
378 "$( stat -c "%y %n" "${CWD}/$recipe" )" \
379 "$( stat -c "%y %n" "${outdir}/${full_pkgname}.tlz" )" \
380 "" \
381 " This recipe will be processed ..." \
383 elif test -e "${CWD}/post-install" && \
384 is_newer "${CWD}/post-install" "${CWD}/$recipe"
385 then
386 warn \
387 "" \
388 "The post-install script is more RECENT than the recipe:" \
389 "" \
390 "$( stat -c "%y %n" "${CWD}/post-install" )" \
391 "$( stat -c "%y %n" "${CWD}/$recipe" )" \
392 "" \
393 " This recipe will be re-processed ..." \
395 touch "${CWD}/$recipe"
396 elif test "$opt_skipqsts" = opt_skipqsts
397 then
398 warn "Recipe for '${full_pkgname}.tlz': [Ignored]." ""
399 return 0
400 else
401 warn \
402 "" \
403 "This recipe ALREADY produced a package:" \
404 "$( stat -c "%y %n" "${outdir}/${full_pkgname}.tlz" )" \
405 "" \
406 "The recipe is still OLDER than the produced package:" \
407 "$( stat -c "%y %n" "${CWD}/$recipe" )" \
408 "" \
409 " Probably nothing has changed." \
412 # In non-interactive mode the user is asked about
413 # rebuilding the package; on interactive mode,
414 # the user need to pass the option explicitly.
416 if test -t 0
417 then
418 warn "Use the --force option to reprocess ${CWD}/${recipe}." ""
419 return 6;
422 printf '%s\n' \
423 "Do you want to rebuild this package? ..." \
424 "(1) Yes, built it" \
425 "(2) No, skip it. [Default]" \
426 "(3) Resume (skipping completed recipes)" \
427 "Enter an option number:" > /dev/tty
428 IFS= read -r ANSWER < /dev/tty || exit 2;
429 case $ANSWER in
431 echo "$ANSWER" > /dev/tty
432 unset -v ANSWER
435 unset -v ANSWER
436 echo "=== Building unprocessed or recently modified recipe(s) ..." > /dev/tty
437 echo ""
438 opt_skipqsts=opt_skipqsts
439 readonly opt_skipqsts
440 return 0
443 unset -v ANSWER
444 echo "Recipe for '${full_pkgname}.tlz': Cancelled." > /dev/tty
445 echo ""
446 return 0
448 esac
452 # Fetch remote sources
454 echo "=== Fetching remote sources if needed ..."
455 if test -n "$fetch"
456 then
457 for origin in $fetch
459 echo "=== Looking for $origin ..."
461 _source="${origin##*/}"; # Get the file name.
463 echo "=== Verifying checksum file found \`${tardir}/${_source}.sha256'"
464 if test -e "${tardir}/${_source}.sha256"
465 then
466 ( cd -- "$tardir" && sha256sum -c "${_source}.sha256" )
467 chkstatus_or_exit
468 continue;
471 warn " Checksum file \`${_source}.sha256' does not exist"
473 # Download source or resume, if allowed
475 if test ! -e "${tardir}/$_source"
476 then
477 warn "=== Attempting to get it from $origin ..."
480 case $origin in
481 rsync://*)
483 cd -- "$tardir" && $rsync "$origin" || exit $?
484 sha256sum "$_source" > "${_source}.sha256"
485 ); chkstatus_or_exit 10
487 *://*)
489 cd -- "$tardir" && $netget "$origin" || exit $?
490 sha256sum "$_source" > "${_source}.sha256"
491 ); chkstatus_or_exit 10
494 warn "${PROGRAM}: Unrecognized protocol for ${origin}."
495 exit 4
496 esac
497 done
498 unset -v origin _source
499 else
500 warn "${recipe}: The variable 'fetch' is empty."
503 # Prepare special directories for build the source,
504 # the destination and the output of the package
506 echo "=== Preparing directories ..."
508 if test -z "$keep_srcdir"
509 then
510 if test -e "$srcdir"
511 then
512 rm -rf -- "$srcdir" || chkstatus_or_exit
513 echo "removed directory: '$srcdir'"
515 else
516 warn "WARNING: The variable 'keep_srcdir' has been set (${keep_srcdir})."
519 if test -z "$keep_destdir"
520 then
521 if test -e "$destdir"
522 then
523 rm -r -- "$destdir" || chkstatus_or_exit
524 echo "removed directory: '$destdir'"
526 make_directory "$destdir"
527 else
528 warn "WARNING: The variable 'keep_destdir' has been set (${keep_destdir})."
531 make_directory "$outdir"
533 echo "=== Changing to '${TMPDIR}' ..."
534 cd -- "$TMPDIR" || chkstatus_or_exit
536 # Set trap before to run the build() function in order
537 # to catch the return status, exit code 2 if fails
539 trap 'chkstatus_or_exit 2' EXIT HUP INT QUIT ABRT TERM
541 # Determine if the debugging indicators of the shell should be
542 # retained, assuming that it has been previously passed
543 case $- in *x*)
544 _xtrace_flag=_xtrace_flag_is_set ;;
545 esac
547 echo "=== Running the 'build' function ..."
548 build
549 unset -f build
551 # Check recipe to run (extra) defined functions by the packager
552 for _definition in \
553 $(awk '!/^build[ (]/ && /^[^ {}]+ *\(\)/{ gsub(/[()]/, "", $1); print $1 }' "${CWD}/$recipe")
555 # Check if it is a shell function
556 case $(LC_ALL=C type $_definition) in
557 *function*)
558 # Call and undo the function after executing it
559 $_definition
560 unset -f $_definition
562 esac
563 done
564 unset -v _definition
566 # Turn off possible shell flags coming from the recipe
568 set +e
569 if test "${_xtrace_flag:+$_xtrace_flag}" != _xtrace_flag_is_set
570 then
571 set +x
574 # Reset given signals
575 trap - EXIT HUP INT QUIT ABRT TERM
577 # If 'destdir' is empty, the package won't be created
578 if rmdir -- "$destdir" 2> /dev/null
579 then
580 warn "The package \"${full_pkgname}.tlz\" won't be created. 'destdir' is empty."
581 opt_nopkg=opt_nopkg
584 # Create (make) the package
586 if test "$opt_nopkg" != opt_nopkg
587 then
588 # Edit the recipe when 'release' is incremented
589 if test "$opt_incr_release" = opt_incr_release
590 then
591 echo ",s/^\\(release\\)=.*/\\1=${release}/"$'\nw' | \
592 ed "${CWD}/$recipe" || chkstatus_or_exit
595 make_directory "${destdir}/var/lib/qi"
597 # Include a copy of the recipe into the package
598 cp -p "${CWD}/$recipe" \
599 "${destdir}/var/lib/qi/${full_pkgname}.recipe" && \
600 chmod 644 "${destdir}/var/lib/qi/${full_pkgname}.recipe"
601 chkstatus_or_exit
603 # Produce a checksum file for recipe copy
604 cd -- "${destdir}/var/lib/qi" && \
605 sha256sum "${full_pkgname}.recipe" > "${full_pkgname}.recipe.sha256"
606 ); chkstatus_or_exit 4
608 # Detect post-install script for inclusion
610 if test -f "${CWD}/post-install"
611 then
612 echo "${CWD}/post-install: Detected."
613 cp -p "${CWD}/post-install" \
614 "${destdir}/var/lib/qi/${full_pkgname}.sh" && \
615 chmod 644 "${destdir}/var/lib/qi/${full_pkgname}.sh"
616 chkstatus_or_exit
619 # Detect declared package name(s) for later replacement
621 if test -n "$replace"
622 then
623 warn \
624 "=== The following package names has been declared for replacement:" \
625 "$replace"
627 : > "${destdir}/var/lib/qi/${full_pkgname}.replace"
628 for item in $replace
630 printf '%s\n' "$replace" >> \
631 "${destdir}/var/lib/qi/${full_pkgname}.replace"
632 done
633 unset -v item
636 # Create (external) meta file for package information,
637 # make a copy of it for the package database
638 echo "=== Creating meta file ${full_pkgname}.tlz.txt ..."
639 do_meta > "${outdir}/${full_pkgname}.tlz.txt" || chkstatus_or_exit
640 cp -p "${outdir}/${full_pkgname}.tlz.txt" \
641 "${destdir}/var/lib/qi/${full_pkgname}.txt" || chkstatus_or_exit
643 # Produce the package
644 cd -- "$destdir" && mode_create "${outdir}/${full_pkgname}.tlz"
647 # Back to the current working directory
648 cd -- "$CWD" || chkstatus_or_exit
650 echo "=== Deleting 'srcdir' or 'destdir' ..."
651 if test "$opt_keep" != opt_keep
652 then
653 if test -z "$keep_srcdir"
654 then
655 if test -e "$srcdir"
656 then
657 rm -rf -- "$srcdir" || chkstatus_or_exit
658 echo "removed directory: '$srcdir'"
661 if test -z "$keep_destdir"
662 then
663 if test -e "$destdir"
664 then
665 rm -rf -- "$destdir" || chkstatus_or_exit
666 echo "removed directory: '$destdir'"
669 else
670 warn \
671 " The following directories will be preserved:" \
672 "$srcdir" \
673 "$destdir" \
674 "" \
675 "The '--keep' option has been used."
678 # Install or upgrade the package after build
679 if test "$opt_nopkg" != opt_nopkg
680 then
681 if test "$opt_install" = opt_install
682 then
683 mode_install "${outdir}/${full_pkgname}.tlz"
684 elif test "$opt_upgrade" = opt_upgrade
685 then
686 mode_upgrade "${outdir}/${full_pkgname}.tlz"
690 warn "{@} Recipe \"${CWD}/${recipe}\" has been processed." ""
693 mode_create()
695 directory="$(dirname -- "$1")"
697 # Perform sanity checks
699 if ! fnmatch '/?*' "$directory"
700 then
701 warn "${PROGRAM}: Output directory \`${directory}' is not fully qualified"
702 exit 4
704 is_readable "$directory" || exit 4
706 name="$(basename -- "$1")"
708 echo ""
709 echo "{#} Creating package name \`${name}' ..."
711 if test "$name" = "${name%.tlz}"
712 then
713 warn "Package format '$name' not supported." \
714 "It should be \"name_version_architecture-release[@pkgcategory].tlz\""
715 exit 4
718 # If needed, assign default values for compression options
719 tarlz_compression_options="${tarlz_compression_options:=-9 --solid}"
721 # Pass extra options to tarlz(1)
722 if test -n "$SOURCE_DATE_EPOCH"
723 then
724 tarlz_compression_options="$tarlz_compression_options --mtime=@${SOURCE_DATE_EPOCH}"
727 ( umask 022 ; tarlz $tarlz_compression_options -cvf - -- * ) > "${directory}/$name"
728 chkstatus_or_exit 3
730 ( cd -- "$directory" && sha256sum "$name" > "${name}.sha256" )
731 chkstatus_or_exit 4
733 warn "{#} Package \"${name}\" created on ${directory}." ""
735 # Remove used variables
736 unset -v directory name
739 mode_remove()
741 # Read configuration file if --no-rc is not passed
742 test "$opt_readrc" = opt_readrc && readconfig
744 expunge="${packagedir}/$(basename -- "$1" .tlz)"
746 echo ""
747 echo "{<} Removing \`$rootdir${expunge}' ..."
749 # Complain if the package directory cannot be well-read
751 is_readable "$rootdir${expunge}" || exit 4
753 # Validate package directory as such
755 test -d "$rootdir${expunge}" || {
756 warn "Package '$rootdir${expunge}' is not a valid directory."
757 exit 4
760 # Remove package from Graft control
762 # Scan for possible conflicts, stop if arise
763 if test "$opt_prune" != opt_prune
764 then
765 echo "=== Checking for possible conflicts ..."
766 if graft -d -n $graft_r -t "$targetdir" "$expunge" 2>&1 | \
767 grep "^CONFLICT"
768 then
769 warn "" \
770 " A conflict occurred during uninstallation;" \
771 "Unless the --prune option is given, this package will be PRESERVED."
772 exit 6;
776 # Remove objects (files, links or directories) from the target
777 # directory that are in conflict with the package directory
779 echo "=== Pruning any conflict ..."
780 graft -p -D -u $graft_r -t "$targetdir" "$expunge"
781 chkstatus_or_exit 2
783 echo "=== Disabling links ..."
784 graft -d -D -u $graft_v $graft_r -t "$targetdir" "$expunge"
785 chkstatus_or_exit 2
787 # Delete package directory
788 if test "$opt_keep" != opt_keep
789 then
790 echo "=== Deleting package directory ..."
791 if is_readable "${rootdir}$expunge"
792 then
793 rm -r -- "${rootdir}$expunge" || chkstatus_or_exit
794 echo "removed directory: '${rootdir}$expunge'"
798 warn "{<} Package \"${expunge##*/}\" removed from $rootdir${expunge%%/*}." ""
799 unset -v expunge
802 mode_install()
804 validate_file "$1" tlz
806 # Read configuration file if --no-rc is not passed
807 test "$opt_readrc" = opt_readrc && readconfig
809 name="$(basename -- "$1" .tlz)"
811 echo ""
812 echo "{>} Installing package \`${name}.tlz' ..."
814 echo "=== Checking tarball integrity ..."
815 tarlz --missing-crc -tf "$1" > /dev/null
816 chkstatus_or_exit 3
818 # To accept random directory from the upgrade mode
819 _packagedir="$2"
820 _packagedir="${_packagedir:=$packagedir}"
822 make_directory "$rootdir${_packagedir}/$name"
824 # Scan for possible conflicts, stop if arise
825 if test "$opt_prune" != opt_prune
826 then
827 echo "=== Checking for possible conflicts ..."
828 if graft -i -n $graft_r -t "$targetdir" "${_packagedir}/$name" 2>&1 | \
829 grep "^CONFLICT"
830 then
831 warn "" \
832 " A conflict occurred during installation;" \
833 "Unless the --prune option is given, this package won't be LINKED."
834 exit 6;
838 echo "=== Decompressing package ..."
839 ( cd -- "$rootdir${_packagedir}/$name" && tarlz -xpf - ) < "$1"
840 chkstatus_or_exit 3
842 # Transite package to Graft control
844 # Remove objects (files, links or directories) from the target
845 # directory that are in conflict with the package directory
846 echo "=== Pruning any conflict ..."
847 graft -p -D -u $graft_r -t "$targetdir" "${_packagedir}/$name"
848 chkstatus_or_exit 2
850 echo "=== Enabling symbolic links ..."
851 graft -i -P $graft_v $graft_r -t "$targetdir" "${_packagedir}/$name"
852 chkstatus_or_exit 2
854 # Avoid unnecessary runs coming from the upgrade mode,
855 # this is when the incoming package is **pre-installed**
857 if test "$_isUpgrade" != _isUpgrade.on
858 then
859 # Show package description
860 if test -r "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.txt"
861 then
862 awk '/^#/' "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.txt"
863 elif test -r "${1}.txt"
864 then
865 # From external meta file (current directory)
866 awk '/^#/' "${1}.txt"
867 else
868 warn "Description file not found for '$name'."
871 # Check and run the post-install script if exist
872 if test -r "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.sh"
873 then
874 echo "=== Running post-install script for \`${name}' ..."
876 # Rely on 'targetdir' if 'rootdir' is empty
877 cd -- "${rootdir:=$targetdir}"/ && \
878 . "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.sh"
882 # Check if there are declared packages for replacement
883 if test -r "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.replace"
884 then
885 while read -r line
887 for replace in "$rootdir${_packagedir}/${line%%_*}"_*
889 if test ! -e "$replace"
890 then
891 warn " Declared package \`${replace}' to be replaced does not exist. [Ignored]"
892 continue;
895 replace="${replace##*/}"
897 # The search for the package to be replaced cannot
898 # be the same to the incoming package, even to the
899 # temporary location coming from the upgrade mode
900 if test "$replace" = "$name" || \
901 test "_x_${replace}" = "_x_${PRVLOC##*/}"
902 then
903 continue;
906 warn "WARNING: Replacing package \`${replace}' ..."
908 # Since the links belongs to the new package, only
909 # those which are not in conflict can be deleted.
910 # To complete, we will remove the package directory
912 graft -d -D -u $graft_r \
913 -t "$targetdir" "$replace" > /dev/null 2>&1
915 rm -rf -- "$rootdir${_packagedir}/$replace"
916 done
917 done < "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.replace"
921 warn "{>} Package \"${name}\" installed on $rootdir${_packagedir}." ""
922 unset -v name _packagedir; # Remove used variables.
925 mode_order()
927 validate_file "$1" order
929 # Get a clean list of the file while printing its contents in
930 # reverse order. Last awk(1) in the pipeline eliminates
931 # non-consecutive lines: duplicates, blank lines, and colons.
932 # Comment lines beginning with '#' are allowed; parentheses
933 # are simply ignored
935 awk '{
936 gsub( /:|^#(.*)$|\([^)]*)|^$/,"" );
937 for( i=NF; i > 0; i-- ) print $i
938 }' "$1" | awk '!s[$0]++'
941 mode_upgrade()
943 validate_file "$1" tlz
945 # Read configuration file if --no-rc is not passed
946 test "$opt_readrc" = opt_readrc && readconfig
948 incoming="$(basename -- "$1" .tlz)"
950 echo ""
951 echo "{^} Upgrading to package \`${incoming}.tlz' ..."
953 # Check package pre-existence
954 if test "$opt_force" != opt_force && \
955 test -e "$rootdir${packagedir}/$incoming"
956 then
957 warn "" \
958 " The package to be upgraded already exist;" \
959 "Unless the --force option is given, this package won't be UPGRADED."
960 exit 6;
963 # Ignore some signals until the upgrade process is complete
964 trap "" HUP INT QUIT ABRT TERM
966 # Check blacklisted packages before to proceed with the upgrade
968 echo "=== Checking blacklist ..."
969 for item in $blacklist
971 case $item in
972 "${incoming%@*}"*)
973 warn \
974 " A blacklisted package name has been detected:" \
975 "$incoming" \
976 "" \
977 "^ This package will be INSTALLED instead of being upgraded ..."
978 opt_prune=opt_prune mode_install "$1"
979 return 0;
981 esac
982 done
983 unset -v item
985 # Prepare the package to install it in a temporary but custom location
987 # Set random directory under 'rootdir/packagedir' using 'incoming' as name
988 PRVLOC=$(mktemp -dp "$rootdir${packagedir}" ${incoming}.XXXXXXXXXXXX) || exit 2
990 echo "=== Pre-installing package in temporary location ..."
992 opt_prune=opt_prune; # Turns on prune operation.
994 _isUpgrade=_isUpgrade.on mode_install "$1" "$PRVLOC" > /dev/null
995 _isUpgrade=_isUpgrade.off
997 echo "=== Looking for installations under the same name ..."
998 for previous_package in "$rootdir${packagedir}/${incoming%%_*}"_*
1000 test -e "$previous_package" || continue
1002 # Previous package directory to be removed cannot be
1003 # the same to the temporary (private) location
1004 if test "$previous_package" = "$PRVLOC"
1005 then
1006 continue;
1009 # The directory of the package could be preserved if --keep
1010 mode_remove "$previous_package"
1011 done
1012 unset -v previous_package
1014 # Re-install incoming package to the final location
1016 mode_install "$1"
1017 opt_prune=opt_prune.off; # Turns off prune operation.
1019 echo "=== Deleting temporary location ..."
1020 if is_readable "$PRVLOC"
1021 then
1022 rm -rf -- "$PRVLOC" || chkstatus_or_exit
1023 echo "removed directory: '$PRVLOC'"
1025 unset -v PRVLOC
1027 warn "{^} Package \"${incoming}\" upgraded for $rootdir${packagedir}." ""
1028 unset -v incoming
1030 # Reset given signals
1031 trap - HUP INT QUIT ABRT TERM
1034 mode_warn()
1036 validate_file "$1" tlz
1038 echo ""
1039 echo "{!} Warning about the content of \`$(basename -- "$1")' ..."
1041 # List content of files excluding directories
1042 tarlz -tvvf "$1" | awk '!/^drwx/'
1043 chkstatus_or_exit 3
1046 mode_extract()
1048 validate_file "$1" tlz
1050 name="$(basename -- "$1" .tlz)"
1052 echo ""
1053 echo "{-} Extracting from package \`${name}.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 ( umask 000 ; cd -- "$PRVDIR" && tarlz -xvf - ) < "$1"
1067 if test $? -ne 0
1068 then
1069 # Try to remove (empty) 'PRVDIR' on failure
1070 rmdir -- "$PRVDIR"
1071 exit 3;
1074 warn "" "{-} Package \"${name}\" has been extracted on ${PRVDIR}."
1076 # Remove used variables
1077 unset -v name PRVDIR
1079 # Reset given signals
1080 trap - HUP INT ABRT TERM
1083 ### Extra functions to be used during the modes
1085 unpack()
1087 for file in "$@"
1089 case $file in
1090 *.tar)
1091 tar -tf "$file" > /dev/null && tar -xpf "$file"
1092 chkstatus_or_exit 3
1094 *.tar.gz | *.tgz | *.tar.Z )
1095 gzip -cd "$file" | tar -tf - > /dev/null && gzip -cd "$file" | tar -xpf -
1096 chkstatus_or_exit 3
1098 *.tar.bz2 | *.tbz2 | *.tbz )
1099 bzip2 -cd "$file" | tar -tf - > /dev/null && bzip2 -cd "$file" | tar -xpf -
1100 chkstatus_or_exit 3
1102 *.tar.lz | *.tlz )
1103 lzip -cd "$file" | tar -tf - > /dev/null && lzip -cd "$file" | tar -xpf -
1104 chkstatus_or_exit 3
1106 *.tar.xz | *.txz )
1107 xz -cd "$file" | tar -tf - > /dev/null && xz -cd "$file" | tar -xpf -
1108 chkstatus_or_exit 3
1110 *.tar.zst | *.tzst )
1111 zstd -cd "$file" | tar -tf - > /dev/null && zstd -cd "$file" | tar -xpf -
1112 chkstatus_or_exit 3
1114 *.zip | *.ZIP )
1115 unzip -t "$file" > /dev/null && unzip "$file" > /dev/null
1116 chkstatus_or_exit 3
1118 *.gz)
1119 gzip -t "$file" && gzip -cd "$file" > "$(basename -- "$file" .gz)"
1120 chkstatus_or_exit 3
1122 *.Z)
1123 gzip -t "$file" && gzip -cd "$file" > "$(basename -- "$file" .Z)"
1124 chkstatus_or_exit 3
1126 *.bz2)
1127 bzip2 -t "$file" && bzip2 -cd "$file" > "$(basename -- "$file" .bz2)"
1128 chkstatus_or_exit 3
1130 *.lz)
1131 lzip -t "$file" && lzip -cd "$file" > "$(basename -- "$file" .lz)"
1132 chkstatus_or_exit 3
1134 *.xz)
1135 xz -t "$file" && xz -cd "$file" > "$(basename -- "$file" .xz)"
1136 chkstatus_or_exit 3
1138 *.zst)
1139 zstd -qt "$file" && zstd -cd "$file" > "$(basename -- "$file" .zst)"
1140 chkstatus_or_exit 3
1143 warn "${PROGRAM}: cannot unpack ${file}: Unsupported extension"
1144 exit 1
1145 esac
1146 done
1147 unset -v file
1150 do_meta()
1152 # Extract information from the recipe to create the meta file.
1154 # The package description is pre-formatted in 78 columns,
1155 # the '#' character and a space is added as prefix to conform
1156 # the 80 columns in total
1157 printf '%s\n' "$description" | fold -w 78 | awk '$0="# " $0'
1159 # Include build flags only if it is a real architecture
1160 if test "$arch" != noarch
1161 then
1162 printf '%s' \
1164 QICFLAGS=\"$QICFLAGS\"
1165 QICXXFLAGS=\"$QICXXFLAGS\"
1166 QILDFLAGS=\"$QILDFLAGS\"
1167 QICPPFLAGS=\"$QICPPFLAGS\""
1170 # Print saving the rest of the package information
1171 printf '%s' \
1173 pkgname=$pkgname
1174 pkgversion=$pkgversion
1175 arch=$arch
1176 release=$release
1177 pkgcategory=\"${pkgcategory#@*}\"
1178 full_pkgname=$full_pkgname
1179 blurb=\"$(printf '%s\n' "$description" | sed -e '/^$/d;2q')\"
1180 homepage=\"$homepage\"
1181 license=\"$license\"
1182 fetch=\"$fetch\"
1183 replace=\"$replace\"
1187 ### Default values
1189 packagedir=@PACKAGEDIR@
1190 targetdir=@TARGETDIR@
1191 blacklist="perl5 graft tarlz plzip musl glibc coreutils bash mksh"
1192 _rcfile=@SYSCONFDIR@/qirc
1193 opt_readrc=opt_readrc
1194 opt_install=opt_install.off
1195 opt_upgrade=opt_upgrade.off
1196 opt_force=opt_force.off
1197 opt_keep=opt_keep.off
1198 opt_incr_release=opt_incr_release.off
1199 opt_skipqsts=opt_skipqsts.off
1200 opt_nopkg=opt_nopkg.off
1201 opt_prune=opt_prune.off
1202 verbose_level=0
1203 opt_skiprecipe=""
1204 rootdir=""
1205 arch=@ARCH@
1206 jobs=1
1207 mode=""
1208 _xtrace_flag=""
1209 _readstdin=""
1210 graft_v=""
1211 graft_r=""
1212 _isUpgrade=_isUpgrade.off
1213 keep_srcdir=""
1214 keep_destdir=""
1215 TMPDIR="${TMPDIR:-/usr/src/qi/build}"
1216 QICFLAGS="${QICFLAGS:-@QICFLAGS@}"
1217 QICXXFLAGS="${QICXXFLAGS:-@QICXXFLAGS@}"
1218 QILDFLAGS="${QILDFLAGS:-@QILDFLAGS@}"
1219 QICPPFLAGS="${QICPPFLAGS:-@QICPPFLAGS@}"
1220 worktree=/usr/src/qi
1221 tardir=${worktree}/sources
1222 outdir=@OUTDIR@
1223 netget="wget2 -c -w1 -t3 --no-check-certificate"
1224 rsync="rsync -v -a -L -z -i --progress"
1225 tarlz_compression_options="-9 --solid"
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 ### Parse commands and options
1233 validate_mode()
1235 if test -n "$mode"
1236 then
1237 warn "${PROGRAM}: First defined command: ${mode#*_}" \
1238 "Switching to another command is not allowed (${1})."
1239 exit 1
1243 validate_option()
1245 if test -z "$2"
1246 then
1247 warn "${PROGRAM}: The '${1}' option requires an argument" \
1248 "Try '${PROGRAM} --help' for more information."
1249 exit 1
1253 validate_directory()
1255 if test ! -d "$2"
1256 then
1257 warn "${PROGRAM}: Value \"${2}\" from '${1%%=*}' option must be a valid directory name"
1258 exit 1
1262 validate_digit()
1264 name="$1"
1265 shift
1267 # Taken from https://mywiki.wooledge.org/BashFAQ/054
1268 case "${1#[-+]}" in
1270 warn "${PROGRAM}: The '${name}' option has no defined value"
1271 exit 1
1273 *.*.*)
1274 warn "${PROGRAM}: The '${name}' option has more than one decimal point on it \"${1}\""
1275 exit 1
1277 *[!0-9]*)
1278 warn "${PROGRAM}: The '${name}' option contains a non-valid digit on it \"${1}\""
1279 exit 1
1281 esac
1283 unset -v name
1286 while test $# -gt 0
1288 case $1 in
1289 warn)
1290 validate_mode warn
1291 mode=mode_warn
1293 install)
1294 validate_mode install
1295 mode=mode_install
1297 remove)
1298 validate_mode remove
1299 mode=mode_remove
1301 upgrade)
1302 validate_mode upgrade
1303 mode=mode_upgrade
1305 extract)
1306 validate_mode extract
1307 mode=mode_extract
1309 create)
1310 validate_mode create
1311 mode=mode_create
1313 order)
1314 validate_mode order
1315 mode=mode_order
1317 build)
1318 validate_mode build
1319 mode=mode_build
1321 --no-rc | -N )
1322 opt_readrc=opt_readrc.off
1324 --install | -i )
1325 opt_install=opt_install
1327 --upgrade | -u )
1328 opt_upgrade=opt_upgrade
1330 --force | -f )
1331 opt_force=opt_force
1333 --keep | -k )
1334 opt_keep=opt_keep
1336 --prune | -p )
1337 opt_prune=opt_prune
1339 --packagedir | -P )
1340 validate_option "$1" "$2"
1341 packagedir="$2"
1342 validate_directory "$1" "$packagedir"
1343 shift
1345 --packagedir=*)
1346 validate_option "$1" "$2"
1347 packagedir="${1#*=}"
1348 validate_directory "$1" "$packagedir"
1350 --targetdir | -t )
1351 validate_option "$1" "$2"
1352 targetdir="$2"
1353 validate_directory "$1" "$targetdir"
1354 shift
1356 --targetdir=*)
1357 validate_option "$1" "$2"
1358 targetdir="${1#*=}"
1359 validate_directory "$1" "$targetdir"
1361 --rootdir | -r )
1362 validate_option "$1" "$2"
1363 rootdir="$2"
1364 validate_directory "$1" "$rootdir"
1365 shift
1367 --rootdir=*)
1368 validate_option "$1" "$2"
1369 rootdir="${1#*=}"
1370 validate_directory "$1" "$rootdir"
1372 --outdir | -o )
1373 validate_option "$1" "$2"
1374 outdir="$2"
1375 validate_directory "$1" "$outdir"
1376 shift
1378 --outdir=*)
1379 validate_option "$1" "$2"
1380 outdir="${1#*=}"
1381 validate_directory "$1" "$outdir"
1383 --worktree | -w )
1384 validate_option "$1" "$2"
1385 worktree="$2"
1386 validate_directory "$1" "$worktree"
1387 shift
1389 --worktree=*)
1390 validate_option "$1" "$2"
1391 worktree="${1#*=}"
1392 validate_directory "$1" "$worktree"
1394 --sourcedir | -s )
1395 validate_option "$1" "$2"
1396 tardir="$2"
1397 validate_directory "$1" "$tardir"
1398 shift
1400 --sourcedir=*)
1401 validate_option "$1" "$2"
1402 tardir="${1#*=}"
1403 validate_directory "$1" "$tardir"
1405 --architecture | -a )
1406 validate_option "$1" "$2"
1407 arch="$2"
1408 shift
1410 --arch=*)
1411 validate_option "$1" "$2"
1412 arch="${1#*=}"
1414 --jobs | -j )
1415 jobs="$2"
1416 validate_digit "$1" "$jobs"
1417 shift
1419 -j[0-9]*)
1420 jobs="${1#-j*}"
1421 validate_digit '-j' "$jobs"
1423 --jobs=*)
1424 jobs="${1#*=}"
1425 validate_digit '--jobs=' "$jobs"
1427 --no-package | -n )
1428 opt_nopkg=opt_nopkg
1430 --increment | -1 )
1431 opt_incr_release=opt_incr_release
1433 --skip-questions | -S )
1434 opt_skipqsts=opt_skipqsts
1436 --verbose | -v )
1437 verbose_level=$(( verbose_level + 1 ))
1439 -vv)
1440 # A trick for a second -v.
1441 verbose_level=2
1443 --show-location | -L )
1444 test "$opt_readrc" = opt_readrc && readconfig
1445 printf '%s\n' \
1446 "QI_TARGETDIR=$targetdir" \
1447 "QI_PACKAGEDIR=$packagedir" \
1448 "QI_WORKTREE=$worktree" \
1449 "QI_TARDIR=$tardir" \
1450 "QI_OUTDIR=$outdir"
1451 exit 0
1453 --help | --hel | --he | --h | '--?' | -help | -hel | -he | -h | '-?' | help )
1454 usage
1455 exit 0
1457 --version | --versio | --versi | --vers | \
1458 -version | -versio | -versi | -vers | -V | version )
1459 echo "$PROGRAM version @VERSION@"
1460 exit 0
1462 '-')
1463 _readstdin=readstdin
1464 break
1467 shift
1468 break; # End of options.
1471 warn "qi: Unrecognized option: $1" \
1472 "Try '${PROGRAM} --help' for more information."
1473 exit 1
1476 break; # No more options.
1478 esac
1479 shift
1480 done
1481 unset -f validate_mode validate_option validate_directory validate_digit
1483 # When there are no arguments, show the help
1484 if test $# -eq 0
1485 then
1486 usage
1487 exit 1
1489 unset -f usage
1491 # Program sanity check
1493 for need in awk basename chmod cp dirname find fold graft grep \
1494 mkdir mktemp rm rmdir sed sha256sum stat tarlz ; \
1496 if ! \command -v $need > /dev/null
1497 then
1498 warn "${PROGRAM}: Prerequisite \`${need}' not found in PATH"
1499 exit 2
1501 done
1502 unset -v need
1504 # Set verbosity level and flags
1506 if test "$verbose_level" -gt 0
1507 then
1508 if test "$verbose_level" -eq 1
1509 then
1510 graft_v=-v
1511 else
1512 graft_v=-V
1516 # Read standard input if FILE is -, or when
1517 # FILE is not connected to a terminal
1519 if test "$_readstdin" = readstdin
1520 then
1521 if test -t 0
1522 then
1523 warn "qi: I won't read from a connected terminal." \
1524 "Try '${PROGRAM} --help' for more information."
1525 exit 1
1528 # Unset positional parameters setting $# to zero
1529 set --
1531 # Assign remaining arguments to the positional parameters
1532 while read -r input
1534 set -- "$@" "$input"
1535 done
1537 unset -v _readstdin
1539 if test -z "$mode"
1540 then
1541 warn "qi: We need at least one (valid) command." \
1542 "Try '${PROGRAM} --help' for more information."
1543 exit 4
1546 # Validate 'packagedir' and 'targetdir' as canonical directories
1548 # The single slash '/' does not qualify here
1549 if ! fnmatch '/?*' "$packagedir"
1550 then
1551 warn "${PROGRAM}: Package directory \`${packagedir}' is not fully qualified"
1552 exit 4
1554 if test ! -d "$packagedir"
1555 then
1556 warn "${PROGRAM}: Package directory \`${packagedir}' does not exist"
1557 exit 4
1560 # The single slash '/' is valid here
1561 if ! fnmatch '/*' "$targetdir"
1562 then
1563 warn "${PROGRAM}: Target directory \`${targetdir}' is not fully qualified"
1564 exit 4
1566 if test ! -d "$targetdir"
1567 then
1568 warn "${PROGRAM}: Target directory \`${targetdir}' does not exist"
1569 exit 4
1572 # Validate 'rootdir' directory
1574 if test -n "$rootdir"
1575 then
1576 if test -d "$rootdir" && test "$rootdir" != /
1577 then
1578 rootdir="${rootdir%/}" # Remove slash from the end.
1580 # A workaround for graft-2.13+. The specified directory is
1581 # relative to the log file, we prepend it inside 'rootdir'
1583 eval "$(graft -L)" ; GRAFT_LOGFILE="${GRAFT_LOGFILE:=/var/log/graft}"
1584 mkdir -p -- "$rootdir$(dirname -- "$GRAFT_LOGFILE")" || chkstatus_or_exit
1586 # Compose 'rootdir' and log file option to be used with graft(1)
1587 graft_r="-r $rootdir -l $GRAFT_LOGFILE"
1589 # Unset variables coming from eval
1590 unset -v GRAFT_PERL GRAFT_LOGFILE GRAFT_TARGETDIR GRAFT_PACKAGEDIR
1591 else
1592 warn "${PROGRAM}: Root directory \`${rootdir}' is not fully qualified"
1593 exit 4
1595 readonly rootdir
1596 export rootdir
1599 # Ensure 'TMPDIR' creation to prefix temporary files
1600 make_directory "$TMPDIR"
1602 # Do not allow the following variables to be modified
1603 readonly TMPDIR packagedir targetdir opt_readrc
1605 # Process each package or recipe provided on the command-line.
1606 for package in "$@"
1608 $mode "$package"
1609 done