qi: minor changes on code, documentation
[dragora.git] / qi / src / qi.in
blob876cdc5e9f96fdd75ef4fd981875d4717d80b02b
1 #! /bin/sh -
2 # Copyright (C) 2016-2019 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 #### Functions
29 usage()
31 printf "%s\n" \
32 "Qi - A user-friendly package manager." \
33 "" \
34 "Usage: $PROGRAM [OPTION...] [FILE]..." \
35 "" \
36 "Operation mode:" \
37 " -b Build packages using recipe names" \
38 " -c Create .tlz package from directory" \
39 " -d Delete packages" \
40 " -i Install packages" \
41 " -o Resolve build order through .order files" \
42 " -u Update packages (implies -i, -d with -p)" \
43 " -w Warn about files that will be linked" \
44 " -x Extract a package for debugging purposes" \
45 "" \
46 "Common options:" \
47 " -N Do not read the configuration file" \
48 " -P <DIR> Package directory for installations." \
49 " Only valid for -i, -d, or -u options" \
50 " -f This option can force the build of a recipe," \
51 " or force the update of a pre-existing package." \
52 " Only valid for -b, -u options" \
53 " -t <DIR> Target directory for symbolic links." \
54 " Only valid for -i, -d, or -u options" \
55 " -k Keep \`\${srcdir}' or \`\${destdir}' on build mode," \
56 " keep package directory on delete mode." \
57 " Only valid for -b, -d or -u options" \
58 " -p Prune conflicts on package installations" \
59 " -r <DIR> Use the fully qualified named directory as the" \
60 " root directory for all qi operations. The target" \
61 " directory and package directory will be relative to" \
62 " the specified directory, including the log file for" \
63 " graft" \
64 " -v Be verbose (a 2nd -v gives more)" \
65 "" \
66 "Options for 'build' mode (-b):" \
67 " -O <DIR> Where the produced packages are written" \
68 " -W <DIR> Where archives, patches, and recipes are expected" \
69 " -Z <DIR> Where (compressed) sources will be found" \
70 " -a Architecture to use [detected]" \
71 " -j Parallel jobs for the compiler" \
72 " -1 Increment release number (\`\${release}' + 1)" \
73 " -n Don't create a .tlz package" \
74 " -S Selects the option to skip completed recipes" \
75 "" \
76 "Informative options:" \
77 " -L Print default directory locations" \
78 " -h Display this help and exit" \
79 " -V Output version information" \
80 "" \
81 "Some influential environment variables:" \
82 " TMPDIR Temporary directory for sources" \
83 " QICFLAGS C compiler flags" \
84 " QICXXFLAGS C++ compiler flags" \
85 " QILDFLAGS Linker flags" \
86 "" \
87 "When FILE is -, read standard input." \
91 version()
93 printf "%s\n" \
94 "$PROGRAM @VERSION@" \
95 "Copyright (C) 2016-2019 Matias Andres Fonzo." \
96 "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>" \
97 "This is free software: you are free to change and redistribute it." \
98 "There is NO WARRANTY, to the extent permitted by law."
101 warn()
103 printf "%s\n" "$@" 1>&2
106 is_readable()
108 if test -e "$1"
109 then
110 if ! test -r "$1"
111 then
112 echo "${PROGRAM}: cannot read ${1}: Permission denied" 1>&2
113 return 1
115 else
116 echo "${PROGRAM}: cannot access ${1}: No such file or directory" 1>&2
117 return 1
121 # Portable alternative to the file operator -nt (among shells)
122 is_newer()
124 if test -n "$(find $1 -prune -newer $2 -print)"
125 then
126 return 0
128 return 1
131 # Determine whether $2 matches pattern $1
132 fnmatch()
134 case $2 in
136 return 0
139 return 1
141 esac
144 chkstatus_or_exit()
146 status=$?
148 if test $status -ne 0
149 then
150 echo "Return status = $status" 1>&2
151 exit ${1-2}; # If not given, defaults to 2
154 unset status
157 readconfig()
159 if test $RC = RC
160 then
161 is_readable "$HOME/.qirc" 2> /dev/null && RCFILE="$HOME/.qirc";
163 echo "Processing \`${RCFILE}' ..."
165 test -f "$RCFILE" || {
166 warn "${RCFILE} is not a regular file."
167 return 1
170 # Parse config file
171 while IFS='=' read -r variable value
173 case $variable in
174 \#* | "") # Ignore commented or blank lines
175 continue
177 esac
179 # Set variable name avoiding code execution
180 eval "$variable=\${value}"
181 done < "$RCFILE"
185 #### Mode functions
187 build_mode()
189 recipe=$1
191 # A recipe is any valid regular file, the current working directory
192 # has priority over the working tree (or where the recipes reside).
193 # The 'worktree' is the second place where to find a recipe. Also,
194 # we complete the possibility of using the directory name to invoke
195 # a recipe if it contains "recipe" as valid file name.
197 if test ! -f "$recipe"
198 then
199 if test -f "${recipe}/recipe"
200 then
201 recipe="${recipe}/recipe"
202 elif test -f "${worktree}/recipes/${recipe}/recipe"
203 then
204 recipe="${worktree}/recipes/${recipe}/recipe"
208 test -f "$recipe" || {
209 warn "\`${recipe}' is not a regular file."
210 return 4
213 # Complain if the file name is not "recipe"
215 if test "${recipe##*/}" != recipe
216 then
217 warn "\`${recipe}' is not a valid recipe name."
218 return 4
221 # Start preparations to import the recipe
223 # Separate the directory name from the file name,
224 # getting its absolute path and base name
226 CWD=$(CDPATH= cd -P -- $(dirname -- "$recipe") && printf "$PWD")
227 recipe=$(basename -- "$recipe")
229 # Check readability for load the recipe on success
231 is_readable "${CWD}/$recipe" || exit 4
233 # Find target architecture if 'arch' is not set
234 if test -z "$arch"
235 then
236 arch=$(${CC:-cc} -dumpmachine 2> /dev/null) || arch=unknown
237 arch="${arch%%-*}" # Get the rid of target triplet.
240 # Re-create external directories
241 mkdir -p -- "${worktree}/archive" \
242 "${worktree}/patches" \
243 "${worktree}/recipes" \
244 "$tardir"
246 # Variables treatment for the current and next recipe.
248 # Unset special variables that can only be predefined on
249 # the recipe and not coming from ${sysconfdir}/qirc
251 unset srcdir destdir pkgname pkgversion program version release \
252 fetch description homepage license replace full_pkgname \
253 CFLAGS CXXFLAGS LDFLAGS
255 # The following variables must be saved and restored
256 save_arch="${save_arch:=$arch}"
257 save_jobs="${save_jobs:=$jobs}"
258 save_outdir="${save_outdir:=$outdir}"
259 save_opt_nopkg="${save_opt_nopkg:=$opt_nopkg}"
261 # Reset variable values in case of return
262 arch=$save_arch
263 jobs=$save_jobs
264 outdir=$save_outdir
265 opt_nopkg=$save_opt_nopkg
267 # The following variables cannot be redefined on the recipe
268 readonly worktree netget rsync
270 # Import the recipe
272 echo "{@} Building from ${CWD}/$recipe ..."
273 . "${CWD}/$recipe"
275 # Check for required variables
276 if test -z "$program"
277 then
278 warn "${recipe}: The variable 'program' is not defined."
279 exit 5
281 if test -z "$version"
282 then
283 warn "${recipe}: The variable 'version' is not defined."
284 exit 5
286 if test -z "$release"
287 then
288 warn "${recipe}: The variable 'release' is not defined."
289 exit 5
292 # Pre-settings before to start building
294 # Increment the release number if the option was given
295 if test "$opt_incr_release" = opt_incr_release
296 then
297 release=$(( release + 1 ))
300 # Allow the dot as definition for 'tardir'
301 if test "$tardir" = .
302 then
303 tardir="$CWD"
306 # Set default values for the following special variables
308 pkgname="${pkgname:=$program}"
309 pkgversion="${pkgversion:=$version}"
310 srcdir="${srcdir:=${program}-$version}"
311 destdir="${destdir:=${TMPDIR}/package-$pkgname}"
313 # Complete package name adding 'pkgversion-arch+release'
314 full_pkgname="${full_pkgname:=$pkgname-${pkgversion}-${arch}+${release}}"
316 # If a package is going to be created, the existence of a
317 # previous build will be detected and reported. Under normal
318 # conditions the recipe is built as long as it is newer than
319 # the produced package, if not, we warn to the user about it.
320 # Rebuilding the package is possible (through the force ;-)
322 if test "$opt_nopkg" != opt_nopkg && \
323 { test "$opt_force" != opt_force && \
324 test -e "${outdir}/${full_pkgname}.tlz" ; }
325 then
326 if is_newer "${CWD}/$recipe" "${outdir}/${full_pkgname}.tlz"
327 then
328 warn \
329 "" \
330 "The recipe is more RECENT than the detected package:" \
331 "" \
332 "$( stat -c "%y %n" "${CWD}/$recipe" )" \
333 "$( stat -c "%y %n" "${outdir}/${full_pkgname}.tlz" )" \
334 "" \
335 " This recipe will be processed ..." \
337 elif test -e "${CWD}/post-install" && \
338 is_newer "${CWD}/post-install" "${CWD}/$recipe"
339 then
340 warn \
341 "" \
342 "The post-install script is more RECENT than the recipe:" \
343 "" \
344 "$( stat -c "%y %n" "${CWD}/post-install" )" \
345 "$( stat -c "%y %n" "${CWD}/$recipe" )" \
346 "" \
347 " The recipe will be re-processed ..." \
349 touch "${CWD}/$recipe"
350 elif test "$opt_skipqsts" = opt_skipqsts
351 then
352 warn "Recipe for '${full_pkgname}.tlz': Ignored." ""
353 return 0
354 else
355 warn \
356 "" \
357 "This recipe ALREADY produced a package:" \
358 "$( stat -c "%y %n" "${outdir}/${full_pkgname}.tlz" )" \
359 "" \
360 "The recipe is still OLDER than the produced package:" \
361 "$( stat -c "%y %n" "${CWD}/$recipe" )" \
362 "" \
363 " Probably nothing has changed." \
366 # In non-interactive mode, the user is asked about
367 # rebuilding the package. In interactive mode,
368 # the user need to pass the option explicitly
369 if test ! -t 0
370 then
371 printf "%s\n" \
372 "Do you want to rebuild this package?" \
373 "1) Yes, built it" \
374 "2) No, skip it [default]" \
375 "3) Resume, skipping completed recipes" \
376 "Enter an option number:" > /dev/tty
377 IFS= read -r ANSWER < /dev/tty || exit 2;
378 case $ANSWER in
380 echo "$ANSWER" > /dev/tty
381 unset ANSWER
384 unset ANSWER
385 echo "Building UNPROCESSED/MODIFIED recipes ..." > /dev/tty
386 echo ""
387 opt_skipqsts=opt_skipqsts
388 readonly opt_skipqsts
389 return 0
392 unset ANSWER
393 echo "Recipe for '${full_pkgname}.tlz': Cancelled." > /dev/tty
394 echo ""
395 return 0
397 esac
398 else
399 warn "Use the -f option to reprocess ${CWD}/$recipe." ""
400 return 6;
405 # Fetch remote sources
407 echo "Fetching remote sources if needed ..."
408 if test -n "$fetch"
409 then
410 for origin in $fetch
412 _source="${origin##*/}"
414 echo "Looking for \"$_source\" ..."
416 echo "Verifying checksum file \"${_source}.sha256\" from '${tardir}'"
417 if test -e "${tardir}/${_source}.sha256"
418 then
419 ( cd -- "$tardir" && sha256sum - ) < "${tardir}/${_source}.sha256"
420 chkstatus_or_exit
421 continue;
422 else
423 warn "${_source}.sha256: Checksum file does not exist, yet."
426 # Download source or resume if allowed
428 if test ! -e "${tardir}/$_source"
429 then
430 warn " Can't find $_source in ${tardir};" \
431 "attempting to get it from ${origin%/*} ..."
434 case $origin in
435 rsync://*)
437 cd -- "$tardir" && $rsync $origin || exit $?
438 sha256sum $_source > ${_source}.sha256
439 ); chkstatus_or_exit 10
441 *://*)
443 cd -- "$tardir" && $netget $origin || exit $?
444 sha256sum $_source > ${_source}.sha256
445 ); chkstatus_or_exit 10
448 warn "${PROGRAM}: Unrecognized protocol for ${origin}."
449 exit 4
450 esac
451 done
452 unset origin _source
453 else
454 warn "The variable 'fetch' is empty."
457 # Prepare special directories for build the source,
458 # the destination and the output of the package
460 echo "Preparing directories ..."
462 if test -d "${TMPDIR}/$srcdir" && test -z "$keep_srcdir"
463 then
464 rm -rf -- "${TMPDIR}/$srcdir" || chkstatus_or_exit
465 echo "removed directory: '${TMPDIR}/$srcdir'"
468 if test -d "$destdir"
469 then
470 rm -rf -- "$destdir" || chkstatus_or_exit
471 echo "removed directory: '$destdir'"
473 mkdir -p -- "$destdir" || chkstatus_or_exit
474 echo "mkdir: created directory '$destdir'"
476 if test ! -d "$outdir"
477 then
478 mkdir -p -- "$outdir" || chkstatus_or_exit
479 echo "mkdir: created directory '$outdir'"
482 echo "Entering to 'TMPDIR': $TMPDIR ..."
483 cd -- "$TMPDIR" || chkstatus_or_exit
485 # Set trap before to run the build() function in order
486 # to catch the return status, exit code 2 if fails
488 trap 'chkstatus_or_exit 2' EXIT HUP INT QUIT ABRT TERM
490 # Determine if the debugging indicators of the shell should be
491 # retained, assuming that it has been previously passed
492 case $- in *x*)
493 _xtrace_flag_is_set=xtrace_flag_is_set ;;
494 esac
496 echo "Running build() ..."
497 build
498 unset build
500 # Turn off possible shell flags coming from the recipe
502 set +e
503 if test "$_xtrace_flag_is_set" != xtrace_flag_is_set
504 then
505 set +x
508 # Reset given signals
509 trap - EXIT HUP INT QUIT ABRT TERM
511 # If 'destdir' is empty, the package won't be created
512 if rmdir -- "$destdir" 2> /dev/null
513 then
514 warn "The package \"${full_pkgname}.tlz\" won't be created. 'destdir' is empty."
515 opt_nopkg=opt_nopkg
518 # Create (make) the package
520 if test "$opt_nopkg" != opt_nopkg
521 then
522 # Edit the recipe when 'release' is incremented
523 if test "$opt_incr_release" = opt_incr_release
524 then
525 echo ",s/^\(release\)=.*/\1=${release}/"$'\nw' | \
526 ed "${CWD}/$recipe" || chkstatus_or_exit
529 mkdir -p -- "${destdir}/var/lib/qi" || chkstatus_or_exit
531 # Include a recipe copy into the package
532 cp -p "${CWD}/$recipe" \
533 "${destdir}/var/lib/qi/${full_pkgname}.recipe" && \
534 chmod 644 "${destdir}/var/lib/qi/${full_pkgname}.recipe" || chkstatus_or_exit
536 # Detect post-install script for inclusion
538 if test -f "${CWD}/post-install"
539 then
540 echo "${CWD}/post-install: Detected."
541 cp -p "${CWD}/post-install" \
542 "${destdir}/var/lib/qi/${full_pkgname}.sh" && \
543 chmod 644 "${destdir}/var/lib/qi/${full_pkgname}.sh" || chkstatus_or_exit
546 # Detect declared package names for later replacement
548 if test -n "$replace"
549 then
550 warn \
551 "The following package names has been declared for replacement:" \
552 " $replace"
554 rm -f "${destdir}/var/lib/qi/${full_pkgname}.replace"
555 for item in $replace
557 echo "$replace" >> "${destdir}/var/lib/qi/${full_pkgname}.replace"
558 done
559 unset item
562 # Create meta file for the package information
563 echo "Creating meta file ${full_pkgname}.tlz.txt ..."
564 do_meta > "${outdir}/${full_pkgname}.tlz.txt" || chkstatus_or_exit
566 # Make a copy of it for the database
567 cp -p "${outdir}/${full_pkgname}.tlz.txt" \
568 "${destdir}/var/lib/qi/${full_pkgname}.txt" || chkstatus_or_exit
570 # Produce the package
571 cd -- "$destdir" && create_mode "${outdir}/${full_pkgname}.tlz"
574 # Back to the current working directory
575 cd -- "$CWD" || chkstatus_or_exit
577 # Delete 'srcdir' or 'destdir' if -k was not given
578 if test "$opt_keep" != opt_keep
579 then
580 echo "Deleting temporary directories ..."
582 srcdir="${srcdir%%/*}" # Directory name without parents.
584 if test -r "${TMPDIR}/$srcdir"
585 then
586 if test -z "$keep_srcdir"
587 then
589 cd -- "$TMPDIR" && rm -rf -- "$srcdir" && \
590 echo "removed directory: '${TMPDIR}/${srcdir}'"
591 ); chkstatus_or_exit
592 else
593 warn "The variable 'keep_srcdir' has been set;" \
594 "'${TMPDIR}/${srcdir}' will not be deleted."
598 if test -r "$destdir"
599 then
600 rm -rf -- "$destdir" || chkstatus_or_exit
601 echo "removed directory: '$destdir'"
605 # Install or update the package if -i or -u was passed
606 if test "$opt_nopkg" != opt_nopkg
607 then
608 if test "$opt_install" = opt_install
609 then
610 install_mode "${outdir}/${full_pkgname}.tlz"
611 elif test "$opt_update" = opt_update
612 then
613 upgrade_mode "${outdir}/${full_pkgname}.tlz"
617 echo ""
618 echo "All done for ${CWD}/${recipe}."
619 echo ""
622 create_mode()
624 directory=$(dirname -- "$1")
626 # Perform sanity checks
628 if ! fnmatch '/?*' "$directory"
629 then
630 warn "${PROGRAM}: Output directory \`${directory}' is not fully qualified"
631 exit 4
633 is_readable "$directory" || exit 4
635 name=$(basename -- "$1")
637 if test "$name" = "${name%.tlz}"
638 then
639 warn "Package format '$name' not supported." \
640 "It should be \"name-version-architecture+release.tlz\""
641 exit 4
644 echo "{#} Creating package $name ..."
646 ( umask 022 ; tarlz --solid -9 -cvf - * ) > "${directory}/$name"
647 chkstatus_or_exit 3
649 ( cd -- "$directory" && sha256sum $name > ${name}.sha256 )
650 chkstatus_or_exit 4
652 echo "Package created on \`${directory}/${name}'."
653 echo ""
655 # Remove used variables
656 unset directory name
659 delete_mode()
661 expunge="${packagedir}/$(basename -- $1 .tlz)"
663 echo "{<} Deleting package $rootdir${expunge} ..."
665 # Complain if the package directory does not exist
667 test -d "$rootdir${expunge}" || {
668 warn "Package directory '$rootdir${expunge}' does not exist."
669 return 4
672 # Complain if the package directory cannot be well-read
674 is_readable "$rootdir${expunge}" || exit 4
676 # Remove package from Graft control
678 # Scan for possible conflicts, stop if arise
679 if test "$opt_prune" != opt_prune
680 then
681 echo "Checking for possible conflicts ..."
682 if graft -d -n $graft_r -t "$targetdir" "$expunge" 2>&1 | \
683 grep ^CONFLICT
684 then
685 warn "" \
686 " A conflict occurred during uninstallation;" \
687 "Unless the -p option is given, this package will be PRESERVED."
688 return 6;
692 # Ignore some signals up to completing the deinstallation
693 trap "" HUP INT QUIT ABRT TERM
695 # Remove objects (files, links or directories) from the target
696 # directory that are in conflict with the package directory
698 echo "Pruning any conflict ..."
699 graft -p -D -u $graft_r -t "$targetdir" "$expunge"
700 chkstatus_or_exit 2
702 echo "Disabling links ..."
703 graft -d -D -u $graft_v $graft_r -t "$targetdir" "$expunge"
704 chkstatus_or_exit 2
706 # Delete package directory
707 if test "$opt_keep" != opt_keep
708 then
709 echo "Deleting package directory, if it exists as such ..."
711 if test -d "${rootdir}$expunge"
712 then
713 rm -rf -- "${rootdir}$expunge" || chkstatus_or_exit
714 echo "removed directory: '${rootdir}$expunge'"
718 # Reset given signals
719 trap - HUP INT QUIT ABRT TERM
721 # Remove used variables
722 unset expunge
725 install_mode()
727 # Complain if the package cannot be well-read
729 is_readable "$1" || exit 4
731 # Complain if the package does not end in .tlz
733 if ! fnmatch '*.tlz' "$1"
734 then
735 warn "\`${1}' does not end in .tlz"
736 return 4
739 # Make preparations to install the package
741 echo "{>} Installing package $1 ..."
743 # Get the filename
744 name=$(basename -- "$1" .tlz)
746 echo "Checking tarball integrity ..."
747 tarlz -tf "$1" > /dev/null
748 chkstatus_or_exit 3
750 # Create package directory using 'name'
751 if ! test -d "$rootdir${packagedir}/$name"
752 then
753 mkdir -p -- "$rootdir${packagedir}/$name" || chkstatus_or_exit
754 echo "mkdir: created directory '$rootdir${packagedir}/$name'"
757 # Scan for possible conflicts, stop if arise
758 if test "$opt_prune" != opt_prune
759 then
760 echo "Checking for possible conflicts ..."
761 if graft -i -n $graft_r -t "$targetdir" "${packagedir}/$name" 2>&1 | \
762 grep ^CONFLICT
763 then
764 warn "" \
765 " A conflict occurred during installation;" \
766 "Unless the -p option is given, this package won't be LINKED."
767 return 6;
771 # Ignore some signals up to completing the installation
772 trap "" HUP INT QUIT ABRT TERM
774 echo "Decompressing $1 ..."
775 ( cd -- "$rootdir${packagedir}/$name" && tarlz -xf - ) < "$1"
776 chkstatus_or_exit 3
778 # Transite package to Graft control
780 # Remove objects (files, links or directories) from the target
781 # directory that are in conflict with the package directory
782 echo "Pruning any conflict ..."
783 graft -p -D -u $graft_r -t "$targetdir" "${packagedir}/$name"
784 chkstatus_or_exit 2
786 echo "Enabling symbolic links ..."
787 graft -i -P $graft_v $graft_r -t "$targetdir" "${packagedir}/$name"
788 chkstatus_or_exit 2
790 # Avoid unnecessary runs coming from the upgrade_mode(),
791 # this is when the incoming package is **pre-installed**
793 if test "$_isUpgrade" != _isUpgrade.on
794 then
795 # Show package description
796 if test -r "$rootdir${packagedir}/${name}/var/lib/qi/${name}.txt"
797 then
798 grep '^#' "$rootdir${packagedir}/${name}/var/lib/qi/${name}.txt"
799 elif test -r "${1}.txt"
800 then
801 # From external meta file (current directory)
802 grep '^#' "${1}.txt"
803 else
804 warn "Description file not found for '$name'."
807 # Check and run the post-install script if exist
808 if test -r "$rootdir${packagedir}/${name}/var/lib/qi/${name}.sh"
809 then
810 echo "Running post-install script for \`${name}' ..."
812 # Rely on 'targetdir' if 'rootdir' is empty
813 cd -- "${rootdir:-$targetdir}"/ && \
814 . "$rootdir${packagedir}/${name}/var/lib/qi/${name}.sh"
818 # Check if there are declared packages for replacement
819 if test -r "$rootdir${packagedir}/${name}/var/lib/qi/${name}.replace"
820 then
821 while read -r line
823 for replace in "$rootdir${packagedir}/$(pkgbase $line)"-*
825 if ! test -e "$replace"
826 then
827 warn "${replace}: Declared package does not exist. (ignored)"
828 continue;
831 replace="${replace##*/}"
833 # The search for the package to be replaced cannot
834 # be the same to the incoming package, even to the
835 # temporary location coming from the upgrade_mode()
836 if test "$replace" = "$name" || test "$replace" = "${PRVLOC##*/}"
837 then
838 continue;
841 warn "WARNING: Replacing package \`${replace}' ..."
843 # Since the links belongs to the new package, only
844 # those which are not in conflict can be deleted.
845 # To complete, we will remove the package directory
847 graft -d -D -u $graft_r \
848 -t "$targetdir" "$replace" > /dev/null 2>&1
850 rm -rf -- "$rootdir${packagedir}/$replace"
851 done
852 done < "$rootdir${packagedir}/${name}/var/lib/qi/${name}.replace"
853 unset line
857 # Reset given signals
858 trap - HUP INT QUIT ABRT TERM
860 # Remove used variables
861 unset name
864 resolve_mode() {
865 # Complain if the file cannot be well-read
867 is_readable "$1" || exit 4
869 # Complain if the file does not end in .order
871 if ! fnmatch '*.order' "$1"
872 then
873 warn "\`${1}' does not end in .order"
874 return 4
877 # Get a clean list of the file while prints its content in reverse order,
878 # lines containing: colons, comments, parentheses, end of line, and blank
879 # lines, are removed. The parentheses are used to insert a reference.
880 # The last `awk' in the pipe: removes nonconsecutive lines, duplicate.
881 awk \
882 '{ gsub( /:|^#(.*)$|\([^)]*)|^$/,"" ); for( i=NF; i > 0; i-- ) print $i }' \
883 "$1" | awk '!s[$0]++'
886 upgrade_mode()
888 # Complain if the package does not end in .tlz
890 if ! fnmatch '*.tlz' "$1"
891 then
892 warn "\`${1}' does not end in .tlz"
893 return 4
896 # Get the filename
897 incoming=$(basename -- "$1" .tlz)
899 echo "{^} Upgrading to $incoming ..."
901 # Check packages in the blacklist for installation
903 echo "Checking blacklist ..."
904 for item in $blacklist
906 case $item in
907 ${incoming}*)
908 if test ! -e "$rootdir${packagedir}/$incoming"
909 then
910 warn \
911 "" \
912 " The package declared in the blacklist will be" \
913 "installed instead of being updated ..." \
915 opt_prune=opt_prune install_mode "$1"
916 return 0
919 if is_newer "$1" "$rootdir${packagedir}/$incoming"
920 then
921 warn \
922 "" \
923 "Incoming package is more RECENT than the installed package:" \
924 "" \
925 "$( stat -c "%y %n" "$1" )" \
926 "$( stat -c "%y %n" "$rootdir${packagedir}/$incoming" )" \
927 "" \
928 " The package declared in the blacklist will be" \
929 " installed as part of the upgrade process ..." \
931 opt_prune=opt_prune install_mode "$1"
932 touch "$rootdir${packagedir}/$incoming"
933 return 0
934 else
935 warn \
936 "" \
937 "Blacklisted package is already up-to-date:" \
938 "" \
939 "$( stat -c "%y %n" "$rootdir${packagedir}/$incoming" )" \
940 "$( stat -c "%y %n" "$1" )" \
942 return 6
945 esac
946 done
947 unset item
949 # Check package pre-existence
950 if test "$opt_force" != opt_force && \
951 test -e "$rootdir${packagedir}/$incoming"
952 then
953 warn \
954 "" \
955 " The package to be updated already exist;" \
956 "Unless the -f option is given, this package won't be UPGRADED."
957 return 6;
960 # Prepare the package to install it in a temporary location
962 # Set random directory using packagedir as prefix and 'incoming' as suffix
963 PRVLOC=$(mktemp -dp "$rootdir${packagedir}" ${incoming}.XXXXXXXXXXXX) || exit 2
965 # Pre-install the package in the custom 'packagedir'
967 save_packagedir="$rootdir${packagedir}"
968 packagedir="$PRVLOC"
970 echo "Pre-installing package in a temporary location ..."
971 opt_prune=opt_prune # Turn on prune operation.
972 _isUpgrade=_isUpgrade.on install_mode "$1" > /dev/null
973 _isUpgrade=_isUpgrade.off
975 # Restore variable before looking for old packages
976 packagedir=$save_packagedir
977 unset save_packagedir
979 echo "Looking for installations under the same name ..."
980 for long_name in "$rootdir${packagedir}/$(pkgbase $incoming)"*
982 found="${long_name##*/}"
984 # The search for the package to be deleted
985 # cannot be the same to the temporary location
986 test "$long_name" = "$PRVLOC" && continue;
988 fnmatch "$(pkgbase $found)*" "$incoming" || continue;
989 echo "${long_name}: Detected."
991 # A package directory is preserved if -k is given
992 delete_mode "$found" > /dev/null
993 done
994 unset long_name found
996 # Re-install the package removing the temporary location
998 install_mode "$1"
999 opt_prune=opt_prune.off # Turn off prune operation.
1001 echo "Deleting temporary location ..."
1002 rm -rf -- "$PRVLOC" || chkstatus_or_exit
1003 echo "removed directory: '$PRVLOC'"
1005 echo ""
1006 echo "Successful upgrade to '${incoming}'."
1008 # Remove remaining variables
1009 unset incoming PRVLOC
1012 warn_mode()
1014 # Complain if the package cannot be well-read
1016 is_readable "$1" || exit 4
1018 # Complain if the package does not end in .tlz
1020 if ! fnmatch '*.tlz' "$1"
1021 then
1022 warn "\`${1}' does not end in .tlz"
1023 return 4
1026 # List content of files excluding directories
1027 while test -f "$1"
1029 tarlz -tvvf "$1" | awk '!/^drwx/'
1030 chkstatus_or_exit 3
1031 shift;
1032 done
1033 return 0
1036 extract_mode()
1038 # Perform sanity checks before package extraction
1040 is_readable "$1" || exit 4
1042 test -f "$1" || {
1043 warn "\`${1}' is not a regular file."
1044 return 4
1047 # Preparations to extract the package
1049 name=$(basename -- "$1" .tlz)
1051 # Set random directory using 'name' as prefix
1052 PRVDIR="${TMPDIR}/${name}.${RANDOM-0}$$"
1054 # Trap to remove 'PRVDIR' on disruptions
1055 trap "rm -rf -- $PRVDIR" HUP INT ABRT TERM
1057 # Create 'PRVDIR' removing access for all but user
1058 ( umask 077 ; mkdir -- $PRVDIR )
1059 mkdir -p -m 700 -- $PRVDIR
1061 echo "Extracting package $name ..."
1062 ( umask 000 ; cd -- $PRVDIR && tarlz -xvf - ) < "$1"
1063 if test $? -ne 0
1064 then
1065 # Try to remove (empty) 'PRVDIR' on failure
1066 rmdir -- $PRVDIR
1067 exit 3;
1069 echo "$name has been extracted on $PRVDIR"
1071 # Reset given signals
1072 trap - HUP INT ABRT TERM
1074 # Remove used variables
1075 unset name PRVDIR
1078 #### Extra functions used in the modes
1080 pkgbase()
1082 string=$(basename -- "$1" .tlz)
1084 # Match cases to print the package name.
1086 # We will take into account the four segments removing
1087 # the last two to print the package (long) name
1088 case $string in
1089 *-*-*+*)
1090 echo "${string%-*-*}"
1093 echo "$string"
1095 esac
1097 unset string
1100 unpack()
1102 for file in "$@"
1104 case $file in
1105 *.tar)
1106 tar -tf "$file" > /dev/null && \
1107 tar -xpf "$file"
1108 chkstatus_or_exit 3
1110 *.tar.gz | *.tgz | *.tar.Z )
1111 gzip -cd "$file" | tar -tf - > /dev/null && \
1112 gzip -cd "$file" | tar -xpf -
1113 chkstatus_or_exit 3
1115 *.tar.bz2 | *.tbz2 | *.tbz )
1116 bzip2 -cd "$file" | tar -tf - > /dev/null && \
1117 bzip2 -cd "$file" | tar -xpf -
1118 chkstatus_or_exit 3
1120 *.tar.lz | *.tlz )
1121 tarlz -tf "$file" > /dev/null && \
1122 tarlz -xf "$file"
1123 chkstatus_or_exit 3
1125 *.tar.xz | *.txz )
1126 xz -cd "$file" | tar -tf - > /dev/null && \
1127 xz -cd "$file" | tar -xpf -
1128 chkstatus_or_exit 3
1130 *.zip | *.ZIP )
1131 unzip -t "$file" > /dev/null && \
1132 unzip "$file" > /dev/null
1133 chkstatus_or_exit 3
1135 *.gz)
1136 gzip -t "$file" && \
1137 gzip -cd "$file" > "$(basename -- $file .gz)"
1138 chkstatus_or_exit 3
1140 *.Z)
1141 gzip -t "$file" && \
1142 gzip -cd "$file" > "$(basename -- $file .Z)"
1143 chkstatus_or_exit 3
1145 *.bz2)
1146 bzip2 -t "$file" && \
1147 bzip2 -cd "$file" > "$(basename -- $file .bz2)"
1148 chkstatus_or_exit 3
1150 *.lz)
1151 lzip -t "$file" && \
1152 lzip -cd "$file" > "$(basename -- $file .lz)"
1153 chkstatus_or_exit 3
1155 *.xz)
1156 xz -t "$file" && \
1157 xz -cd "$file" > "$(basename -- $file .xz)"
1158 chkstatus_or_exit 3
1161 warn "${PROGRAM}: cannot unpack ${file}: Unsupported extension"
1162 exit 1
1163 esac
1164 done
1165 unset file
1168 do_meta()
1170 # Extract information from the recipe to create the meta file.
1172 # The package description is pre-formatted in 78 columns,
1173 # the '#' character and a space is added as prefix to conform
1174 # 80 columns in total
1176 cat << EOF
1177 $(echo "$description" | fold -w 78 | awk '$0="# " $0')
1179 QICFLAGS="$QICFLAGS"
1180 QICXXFLAGS="$QICXXFLAGS"
1181 QILDFLAGS="$QILDFLAGS"
1182 program=$program
1183 version=$version
1184 release=$release
1185 blurb="$(echo "$description" | sed -e '/^$/d;2q')"
1186 homepage="$homepage"
1187 license="$license"
1188 fetch="$fetch"
1189 replace="$replace"
1194 #### Default values
1196 PROGRAM="${0##*/}"
1197 packagedir=@PACKAGEDIR@
1198 targetdir=@TARGETDIR@
1199 blacklist="perl graft tarlz plzip musl glibc"
1200 RC=RC
1201 RCFILE=@SYSCONFDIR@/qirc
1202 opt_install=opt_install.off
1203 opt_update=opt_update.off
1204 opt_force=opt_force.off
1205 opt_keep=opt_keep.off
1206 opt_incr_release=opt_incr_release.off
1207 opt_skipqsts=opt_skipqsts.off
1208 opt_nopkg=opt_nopkg.off
1209 opt_prune=opt_prune.off
1210 rootdir=""
1211 jobs=1
1212 mode=""
1213 verbose=0
1214 graft_v=-v
1215 graft_r=""
1216 _isUpgrade=_isUpgrade.off
1217 TMPDIR="${TMPDIR:=/usr/src/qi/build}"
1218 QICFLAGS="${QICFLAGS:=-g0 -Os}"
1219 QICXXFLAGS="${QICXXFLAGS:=$QICFLAGS}"
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 options
1240 while getopts :bcdiouwxLNP:t:fkvO:W:Z:a:j:1nSpr:hV name
1242 case $name in
1244 if test -z "$mode"
1245 then
1246 readconfig
1247 mode=build_mode
1251 mode=create_mode
1254 readconfig
1255 mode=delete_mode
1258 if test -z "$mode"
1259 then
1260 readconfig
1261 mode=install_mode
1263 if test "$mode" = build_mode
1264 then
1265 opt_install=opt_install
1269 mode=resolve_mode
1272 if test -z "$mode"
1273 then
1274 readconfig
1275 mode=upgrade_mode
1277 if test "$mode" = build_mode
1278 then
1279 opt_update=opt_update
1283 mode=warn_mode
1286 mode=extract_mode
1289 printf "%s\n" \
1290 "QI_TARGETDIR=$QI_TARGETDIR" \
1291 "QI_PACKAGEDIR=$QI_PACKAGEDIR" \
1292 "QI_WORKTREE=$QI_WORKTREE" \
1293 "QI_TARDIR=$QI_TARDIR" \
1294 "QI_OUTDIR=$QI_OUTDIR"
1295 exit 0
1298 RC=RC.off
1301 packagedir="$OPTARG"
1304 targetdir="$OPTARG"
1307 opt_force=opt_force
1310 opt_keep=opt_keep
1313 verbose=$(( verbose + 1 ))
1316 outdir="$OPTARG"
1319 worktree="$OPTARG"
1322 tardir="$OPTARG"
1325 arch="$OPTARG"
1328 jobs="$OPTARG"
1331 opt_incr_release=opt_incr_release
1334 opt_nopkg=opt_nopkg
1337 opt_skipqsts=opt_skipqsts
1340 opt_prune=opt_prune
1343 rootdir="$OPTARG"
1346 usage
1347 exit 0
1350 version
1351 exit 0
1354 warn "Option '-${OPTARG}' requires an argument"
1355 usage
1356 exit 1
1359 warn "Illegal option -- '-${OPTARG}'"
1360 usage
1361 exit 1
1363 esac
1364 done
1365 shift $(( OPTIND - 1 ))
1367 if test $# -eq 0
1368 then
1369 usage
1370 exit 1
1373 # Program sanity check
1374 for need in awk basename chmod cp dirname find fold graft grep \
1375 mkdir mktemp rm rmdir sed sha256sum stat tarlz
1377 if ! type $need 1> /dev/null 2> /dev/null
1378 then
1379 warn "${PROGRAM}: Prerequisite \`${need}' not found in PATH"
1380 exit 2
1382 done
1383 unset need
1385 # Determine verbosity level/flag
1387 if test "$verbose" -gt 1
1388 then
1389 graft_v=-V
1392 # Read standard input if FILE is -, or when FILE
1393 # is not connected to a terminal.
1395 if test "$1" = - || test ! -t 0
1396 then
1397 # Unset positional parameters setting $# to zero
1398 set --
1400 # Assign remaining arguments to the positional parameters
1401 while read -r input
1403 set -- "$@" "$input"
1404 done
1407 # We need at least one operating mode
1408 if test -z "$mode"
1409 then
1410 usage
1411 exit 4
1414 umask 022; # Remove write permission for group and other.
1416 # Validate 'packagedir' and 'targetdir' as canonical directories
1418 # The single slash '/' does not qualify here
1419 if ! fnmatch '/?*' "$packagedir"
1420 then
1421 warn "${PROGRAM}: Package directory \`${packagedir}' is not fully qualified"
1422 exit 4
1424 if test ! -d "$packagedir"
1425 then
1426 warn "${PROGRAM}: Package directory \`${packagedir}' does not exist"
1427 exit 4
1430 # The single slash '/' is valid here
1431 if ! fnmatch '/*' "$targetdir"
1432 then
1433 warn "${PROGRAM}: Target directory \`${targetdir}' is not fully qualified"
1434 exit 4
1436 if test ! -d "$targetdir"
1437 then
1438 warn "${PROGRAM}: Target directory \`${targetdir}' does not exist"
1439 exit 4
1442 # Validate 'rootdir' directory
1444 if test -n "$rootdir"
1445 then
1446 if test -d "$rootdir" && test "$rootdir" != /
1447 then
1448 # Remove slash from the end
1449 rootdir="${rootdir%/}"
1451 # A workaround for graft-2.13+. The specified directory is
1452 # relative to the log file, we prepend it inside the rootdir
1454 eval "$(graft -L)" ; GRAFT_LOGFILE="${GRAFT_LOGFILE:=/var/log/graft}"
1455 mkdir -p -- "$rootdir$(dirname -- $GRAFT_LOGFILE)" || chkstatus_or_exit
1457 # Compose 'rootdir' and log file option to be used with graft(1)
1458 graft_r="-r $rootdir -l $GRAFT_LOGFILE"
1460 # Unset variables coming from eval
1461 unset GRAFT_PERL GRAFT_LOGFILE GRAFT_TARGETDIR GRAFT_PACKAGEDIR
1462 else
1463 warn "${PROGRAM}: Root directory \`${rootdir}' is not fully qualified"
1464 exit 4
1466 export rootdir
1469 # Ensure 'TMPDIR' creation to prefix temporary files
1471 if test ! -d "$TMPDIR"
1472 then
1473 mkdir -p -- "$TMPDIR" || chkstatus_or_exit
1475 readonly TMPDIR
1477 # Process each package or recipe provided on the command-line
1479 for package in "$@"
1481 $mode $package
1482 done