qi: Bump to version 1.5 (Release Candidate 1)
[dragora.git] / qi / src / qi.in
blobcdd8fa489dbd9859580315656f2b6e4bc87fe82f
1 #! /bin/sh -
2 # Copyright (C) 2016-2020 Matias Fonzo <selk@dragora.org>
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 # EXIT STATUS
18 # 0 = Successful completion
19 # 1 = Minor common errors (e.g: help usage, support not available)
20 # 2 = Command execution error
21 # 3 = Integrity check error for compressed files
22 # 4 = File empty, not regular, or expected
23 # 5 = Empty or not defined variable
24 # 6 = Package already installed
25 # 10 = Network manager error
27 \unalias -a; # Unset all possible aliases.
28 LC_ALL=C; # Override locale settings.
29 IFS='
30 '; # Reset Internal Field Separator.
31 umask 022; # Remove write permission for group and other.
33 ### Functions
35 usage()
37 printf "%s\n" \
38 "Qi - A user-friendly package manager." \
39 "" \
40 "Usage: $PROGRAM [OPTION...] [FILE]..." \
41 "" \
42 "Operation mode:" \
43 " -b Build packages using recipe names" \
44 " -c Create .tlz package from directory" \
45 " -d Delete packages" \
46 " -i Install packages" \
47 " -o Resolve build order through .order files" \
48 " -u Upgrade packages (implies -i, -d with -p)" \
49 " -w Warn about files that will be linked" \
50 " -x Extract a package for debugging purposes" \
51 "" \
52 "Common options:" \
53 " -N Do not read the configuration file" \
54 " -P <DIR> Package directory for installations." \
55 " Only valid for -i, -d, or -u options" \
56 " -f This option can force the build of a recipe," \
57 " or force the upgrade of a pre-existing package." \
58 " Only valid for -b, -u options" \
59 " -t <DIR> Target directory for symbolic links." \
60 " Only valid for -i, -d, or -u options" \
61 " -k Keep \`\${srcdir}' or \`\${destdir}' in build mode," \
62 " keep package directory in delete mode." \
63 " Only valid for -b, -d or -u options" \
64 " -p Prune conflicts on package installations" \
65 " -r <DIR> Use the fully qualified named directory as the" \
66 " root directory for all qi operations. The target" \
67 " directory and package directory will be relative to" \
68 " the specified directory, including Graft's log file" \
69 " -v Be verbose (a 2nd -v gives more)" \
70 "" \
71 "Options for 'build' mode (-b):" \
72 " -O <DIR> Where the packages produced are written" \
73 " -W <DIR> Where archives, patches, and recipes are expected" \
74 " -Z <DIR> Where (compressed) sources will be found" \
75 " -a Architecture to use [detected]" \
76 " -j Parallel jobs for the compiler" \
77 " -1 Increment release number (\`\${release}' + 1)" \
78 " -n Don't create a .tlz package" \
79 " -S Selects the option to skip completed recipes" \
80 "" \
81 "Informative options:" \
82 " -L Print default directory locations" \
83 " -h Display this help and exit" \
84 " -V Output version information" \
85 "" \
86 "Some influential environment variables:" \
87 " TMPDIR Temporary directory for sources" \
88 " QICFLAGS C compiler flags" \
89 " QICXXFLAGS C++ compiler flags" \
90 " QILDFLAGS Linker flags" \
91 "" \
92 "When FILE is -, read standard input." \
96 version()
98 printf "%s\n" \
99 "$PROGRAM @VERSION@" \
100 "Copyright (C) 2016-2020 Matias Andres Fonzo." \
101 "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>" \
102 "This is free software: you are free to change and redistribute it." \
103 "There is NO WARRANTY, to the extent permitted by law."
106 warn()
108 printf "%s\n" "$@" 1>&2
111 is_readable()
113 if test -e "$1"
114 then
115 if ! test -r "$1"
116 then
117 echo "${PROGRAM}: cannot read ${1}: Permission denied" 1>&2
118 return 1
120 else
121 echo "${PROGRAM}: cannot access ${1}: No such file or directory" 1>&2
122 return 1
126 # Portable alternative to the file operator -nt (among shells)
127 is_newer()
129 if test -n "$(find $1 -prune -newer $2 -print)"
130 then
131 return 0
133 return 1
136 # Determine whether $2 matches pattern $1
137 fnmatch()
139 case $2 in
141 return 0
144 return 1
146 esac
149 chkstatus_or_exit()
151 status=$?
153 if test $status -ne 0
154 then
155 echo "^ Return status = $status" 1>&2
156 exit ${1-2}; # If not given, defaults to 2
159 unset status
162 readconfig()
164 if test $RC = RC
165 then
166 is_readable "$HOME/.qirc" 2> /dev/null && RCFILE="$HOME/.qirc";
168 echo "Importing configuration file from \`${RCFILE}' ..."
170 test -f "$RCFILE" || {
171 warn "${RCFILE} is not a regular file."
172 return 1
175 . "$RCFILE"
179 ### Mode functions
181 build_mode()
183 recipe=$1
185 # A recipe is any valid regular file. Qi sets priorities for reading a
186 # recipe, the order in which qi looks for a recipe is:
188 # 1. Current working directory.
190 # 2. If the specified path name does not contain "recipe" as the last
191 # component. Qi will complete it by adding "recipe" to the path
192 # name.
194 # 3. If the recipe is not in the current working directory, it will be
195 # searched under '${worktree}/recipes'. The last component will be
196 # completed adding "recipe" to the specified path name.
198 if test ! -f "$recipe"
199 then
200 if test -f "${recipe}/recipe"
201 then
202 recipe="${recipe}/recipe"
203 elif test -f "${worktree}/recipes/${recipe}/recipe"
204 then
205 recipe="${worktree}/recipes/${recipe}/recipe"
209 test -f "$recipe" || {
210 warn "\`${recipe}' is not a regular file."
211 return 4
214 # Complain if the file name is not "recipe"
216 if test "${recipe##*/}" != recipe
217 then
218 warn "\`${recipe}' is not a valid recipe name."
219 return 4
222 # Start preparations to import the recipe
224 # Get working directory and base name of the recipe
226 CWD="$(dirname -- "$recipe")"
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 the next recipe.
248 # Unset special variables that can only be predefined in
249 # the recipe and does not come from '${sysconfdir}/qirc'
251 unset srcdir destdir pkgname pkgversion pkgcategory program version release \
252 fetch description homepage license replace full_pkgname \
253 CFLAGS CXXFLAGS LDFLAGS
255 # The following variables must be restored, later
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"
274 chkstatus_or_exit
276 # Check for required variables
277 if test -z "$program"
278 then
279 warn "${recipe}: The variable 'program' is not defined."
280 exit 5
282 if test -z "$version"
283 then
284 warn "${recipe}: The variable 'version' is not defined."
285 exit 5
287 if test -z "$release"
288 then
289 warn "${recipe}: The variable 'release' is not defined."
290 exit 5
293 # Pre-settings before to start building
295 # Increment the release number if the option was given
296 if test "$opt_incr_release" = opt_incr_release
297 then
298 release=$(( $release + 1 ))
301 # Allow the dot as definition for 'tardir'
302 if test "$tardir" = .
303 then
304 tardir=$CWD
307 # Set default values for the following special variables
309 pkgname="${pkgname:=$program}"
310 pkgversion="${pkgversion:=$version}"
311 srcdir="${srcdir:=${program}-$version}"
312 destdir="${destdir:=${TMPDIR}/package-$pkgname}"
314 # If 'pkgcategory' has been defined, prefix it using "the at symbol"
315 if test -n "$pkgcategory"
316 then
317 pkgcategory='@'${pkgcategory}
320 # Complete package name adding 'pkgversion-arch+release[@pkgcategory]'
321 full_pkgname="${full_pkgname:=$pkgname-${pkgversion}-${arch}+${release}${pkgcategory}}"
323 # Use 'arch' as suffix for 'outdir' to have a well-organized package output
324 outdir=${outdir}/${arch}
326 # If a package is going to be created the existence of a
327 # previous build will be detected and reported. Under normal
328 # conditions the recipe is built as long as it is newer than
329 # the produced package, if not, we warn to the user about it.
330 # Rebuilding the package is possible (through the force ;-)
332 if test "$opt_nopkg" != opt_nopkg && \
333 { test "$opt_force" != opt_force && \
334 test -e "${outdir}/${full_pkgname}.tlz" ; }
335 then
336 if is_newer "${CWD}/$recipe" "${outdir}/${full_pkgname}.tlz"
337 then
338 warn \
339 "" \
340 "The recipe is more RECENT than the detected package:" \
341 "" \
342 "$( stat -c "%y %n" "${CWD}/$recipe" )" \
343 "$( stat -c "%y %n" "${outdir}/${full_pkgname}.tlz" )" \
344 "" \
345 " This recipe will be processed ..." \
347 elif test -e "${CWD}/post-install" && \
348 is_newer "${CWD}/post-install" "${CWD}/$recipe"
349 then
350 warn \
351 "" \
352 "The post-install script is more RECENT than the recipe:" \
353 "" \
354 "$( stat -c "%y %n" "${CWD}/post-install" )" \
355 "$( stat -c "%y %n" "${CWD}/$recipe" )" \
356 "" \
357 " The recipe will be re-processed ..." \
359 touch "${CWD}/$recipe"
360 elif test "$opt_skipqsts" = opt_skipqsts
361 then
362 warn "Recipe for '${full_pkgname}.tlz': Ignored." ""
363 return 0
364 else
365 warn \
366 "" \
367 "This recipe ALREADY produced a package:" \
368 "$( stat -c "%y %n" "${outdir}/${full_pkgname}.tlz" )" \
369 "" \
370 "The recipe is still OLDER than the produced package:" \
371 "$( stat -c "%y %n" "${CWD}/$recipe" )" \
372 "" \
373 " Probably nothing has changed." \
376 # In non-interactive mode, the user is asked about
377 # rebuilding the package. In interactive mode,
378 # the user need to pass the option explicitly
379 if test ! -t 0
380 then
381 printf "%s\n" \
382 "Do you want to rebuild this package?" \
383 "1) Yes, built it" \
384 "2) No, skip it [default]" \
385 "3) Resume, skipping completed recipes" \
386 "Enter an option number:" > /dev/tty
387 IFS= read -r ANSWER < /dev/tty || exit 2;
388 case $ANSWER in
390 echo "$ANSWER" > /dev/tty
391 unset ANSWER
394 unset ANSWER
395 echo "Building UNPROCESSED/MODIFIED recipes ..." > /dev/tty
396 echo ""
397 opt_skipqsts=opt_skipqsts
398 readonly opt_skipqsts
399 return 0
402 unset ANSWER
403 echo "Recipe for '${full_pkgname}.tlz': Cancelled." > /dev/tty
404 echo ""
405 return 0
407 esac
408 else
409 warn "Use the -f option to reprocess ${CWD}/$recipe." ""
410 return 6;
415 # Fetch remote sources
417 echo "Fetching remote sources if needed ..."
418 if test -n "$fetch"
419 then
420 for origin in $fetch
422 _source="${origin##*/}"
424 echo "Looking for \"$_source\" ..."
426 echo "Verifying checksum file \"${_source}.sha256\" from '${tardir}'"
427 if test -e "${tardir}/${_source}.sha256"
428 then
429 ( cd -- "$tardir" && sha256sum - ) < "${tardir}/${_source}.sha256"
430 chkstatus_or_exit
431 continue;
432 else
433 warn "${_source}.sha256: Checksum file does not exist, yet."
436 # Download source or resume if allowed
438 if test ! -e "${tardir}/$_source"
439 then
440 warn " Can't find $_source in ${tardir};" \
441 "attempting to get it from ${origin%/*} ..."
444 case $origin in
445 rsync://*)
447 cd -- "$tardir" && $rsync $origin || exit $?
448 sha256sum $_source > ${_source}.sha256
449 ); chkstatus_or_exit 10
451 *://*)
453 cd -- "$tardir" && $netget $origin || exit $?
454 sha256sum $_source > ${_source}.sha256
455 ); chkstatus_or_exit 10
458 warn "${PROGRAM}: Unrecognized protocol for ${origin}."
459 exit 4
460 esac
461 done
462 unset origin _source
463 else
464 warn "The variable 'fetch' is empty."
467 # Prepare special directories for build the source,
468 # the destination and the output of the package
470 echo "Preparing directories ..."
472 if test -d "${TMPDIR}/$srcdir" && test -z "$keep_srcdir"
473 then
474 rm -r -- "${TMPDIR}/$srcdir" || chkstatus_or_exit
475 echo "removed directory: '${TMPDIR}/$srcdir'"
478 if test -d "$destdir"
479 then
480 rm -r -- "$destdir" || chkstatus_or_exit
481 echo "removed directory: '$destdir'"
483 mkdir -p -- "$destdir" || chkstatus_or_exit
484 echo "mkdir: created directory '$destdir'"
486 if test ! -d "$outdir"
487 then
488 mkdir -p -- "$outdir" || chkstatus_or_exit
489 echo "mkdir: created directory '$outdir'"
492 echo "Entering to 'TMPDIR': $TMPDIR ..."
493 cd -- "$TMPDIR" || chkstatus_or_exit
495 # Set trap before to run the build() function in order
496 # to catch the return status, exit code 2 if fails
498 trap 'chkstatus_or_exit 2' EXIT HUP INT QUIT ABRT TERM
500 # Determine if the debugging indicators of the shell should be
501 # retained, assuming that it has been previously passed
502 case $- in *x*)
503 _xtrace_flag=_xtrace_flag_is_set ;;
504 esac
506 echo "Running build() ..."
507 build
508 unset build
510 # Turn off possible shell flags coming from the recipe
512 set +e
513 if test "${_xtrace_flag:+$_xtrace_flag}" != _xtrace_flag_is_set
514 then
515 set +x
518 # Reset given signals
519 trap - EXIT HUP INT QUIT ABRT TERM
521 # If 'destdir' is empty, the package won't be created
522 if rmdir -- "$destdir" 2> /dev/null
523 then
524 warn "The package \"${full_pkgname}.tlz\" won't be created. 'destdir' is empty."
525 opt_nopkg=opt_nopkg
528 # Create (make) the package
530 if test "$opt_nopkg" != opt_nopkg
531 then
532 # Edit the recipe when 'release' is incremented
533 if test "$opt_incr_release" = opt_incr_release
534 then
535 echo ",s/^\(release\)=.*/\1=${release}/"$'\nw' | \
536 ed "${CWD}/$recipe" || chkstatus_or_exit
539 mkdir -p -- "${destdir}/var/lib/qi" || chkstatus_or_exit
541 # Include a recipe copy into the package
542 cp -p "${CWD}/$recipe" \
543 "${destdir}/var/lib/qi/${full_pkgname}.recipe" && \
544 chmod 644 "${destdir}/var/lib/qi/${full_pkgname}.recipe" || chkstatus_or_exit
546 # Detect post-install script for inclusion
548 if test -f "${CWD}/post-install"
549 then
550 echo "${CWD}/post-install: Detected."
551 cp -p "${CWD}/post-install" \
552 "${destdir}/var/lib/qi/${full_pkgname}.sh" && \
553 chmod 644 "${destdir}/var/lib/qi/${full_pkgname}.sh" || chkstatus_or_exit
556 # Detect declared package names for later replacement
558 if test -n "$replace"
559 then
560 warn \
561 "The following package names has been declared for replacement:" \
562 " $replace"
564 rm -f "${destdir}/var/lib/qi/${full_pkgname}.replace"
565 for item in $replace
567 echo "$replace" >> "${destdir}/var/lib/qi/${full_pkgname}.replace"
568 done
569 unset item
572 # Create meta file for package information
573 echo "Creating meta file ${full_pkgname}.tlz.txt ..."
574 do_meta > "${outdir}/${full_pkgname}.tlz.txt" || chkstatus_or_exit
576 # Make a copy of it for the database
577 cp -p "${outdir}/${full_pkgname}.tlz.txt" \
578 "${destdir}/var/lib/qi/${full_pkgname}.txt" || chkstatus_or_exit
580 # Produce the package
581 cd -- "$destdir" && create_mode "${outdir}/${full_pkgname}.tlz"
584 # Back to the current working directory
585 cd -- "$CWD" || chkstatus_or_exit
587 # Delete 'srcdir' or 'destdir' if -k was not given
588 if test "$opt_keep" != opt_keep
589 then
590 echo "Deleting temporary directories ..."
592 srcdir="${srcdir%%/*}" # Directory name without parents.
594 if test -r "${TMPDIR}/$srcdir"
595 then
596 if test -z "$keep_srcdir"
597 then
599 cd -- "$TMPDIR" && rm -rf -- "$srcdir" && \
600 echo "removed directory: '${TMPDIR}/${srcdir}'"
601 ); chkstatus_or_exit
602 else
603 warn "The variable 'keep_srcdir' has been set;" \
604 "'${TMPDIR}/${srcdir}' will not be deleted."
608 if test -r "$destdir"
609 then
610 rm -rf -- "$destdir" || chkstatus_or_exit
611 echo "removed directory: '$destdir'"
615 # Install or upgrade the package if -i or -u was passed
616 if test "$opt_nopkg" != opt_nopkg
617 then
618 if test "$opt_install" = opt_install
619 then
620 install_mode "${outdir}/${full_pkgname}.tlz"
621 elif test "$opt_upgrade" = opt_upgrade
622 then
623 upgrade_mode "${outdir}/${full_pkgname}.tlz"
627 echo ""
628 echo "All done for ${CWD}/${recipe}."
629 echo ""
632 create_mode()
634 directory="$(dirname -- "$1")"
636 # Perform sanity checks
638 if ! fnmatch '/?*' "$directory"
639 then
640 warn "${PROGRAM}: Output directory \`${directory}' is not fully qualified"
641 exit 4
643 is_readable "$directory" || exit 4
645 name="$(basename -- "$1")"
647 if test "$name" = "${name%.tlz}"
648 then
649 warn "Package format '$name' not supported." \
650 "It should be \"name-version-architecture+release[@pkgcategory].tlz\""
651 exit 4
654 echo "{#} Creating package $name ..."
656 ( umask 022 ; tarlz --solid -9 -cvf - * ) > "${directory}/$name"
657 chkstatus_or_exit 3
659 ( cd -- "$directory" && sha256sum $name > ${name}.sha256 )
660 chkstatus_or_exit 4
662 echo "Package created on \`${directory}/${name}'."
663 echo ""
665 # Remove used variables
666 unset directory name
669 delete_mode()
671 expunge="${packagedir}/$(basename -- "$1" .tlz)"
673 echo "{<} Deleting package $rootdir${expunge} ..."
675 # Complain if the package directory does not exist
677 test -d "$rootdir${expunge}" || {
678 warn "Package directory '$rootdir${expunge}' does not exist."
679 return 4
682 # Complain if the package directory cannot be well-read
684 is_readable "$rootdir${expunge}" || exit 4
686 # Remove package from Graft control
688 # Scan for possible conflicts, stop if arise
689 if test "$opt_prune" != opt_prune
690 then
691 echo "Checking for possible conflicts ..."
692 if graft -d -n $graft_r -t "$targetdir" "$expunge" 2>&1 | \
693 grep ^CONFLICT
694 then
695 warn "" \
696 " A conflict occurred during uninstallation;" \
697 "Unless the -p option is given, this package will be PRESERVED."
698 return 6;
702 # Ignore some signals up to completing the deinstallation
703 trap "" HUP INT QUIT ABRT TERM
705 # Remove objects (files, links or directories) from the target
706 # directory that are in conflict with the package directory
708 echo "Pruning any conflict ..."
709 graft -p -D -u $graft_r -t "$targetdir" "$expunge"
710 chkstatus_or_exit 2
712 echo "Disabling links ..."
713 graft -d -D -u $graft_v $graft_r -t "$targetdir" "$expunge"
714 chkstatus_or_exit 2
716 # Delete package directory
717 if test "$opt_keep" != opt_keep
718 then
719 echo "Deleting package directory, if it exists as such ..."
721 if test -d "${rootdir}$expunge"
722 then
723 rm -rf -- "${rootdir}$expunge" || chkstatus_or_exit
724 echo "removed directory: '${rootdir}$expunge'"
728 # Reset given signals
729 trap - HUP INT QUIT ABRT TERM
731 # Remove used variables
732 unset expunge
735 install_mode()
737 # Complain if the package cannot be well-read
739 is_readable "$1" || exit 4
741 # Complain if the package does not end in .tlz
743 if ! fnmatch '*.tlz' "$1"
744 then
745 warn "\`${1}' does not end in .tlz"
746 return 4
749 # Make preparations to install the package
751 echo "{>} Installing package $1 ..."
753 # Get the filename
754 name="$(basename -- "$1" .tlz)"
756 echo "Checking tarball integrity ..."
757 tarlz -tf "$1" > /dev/null
758 chkstatus_or_exit 3
760 # Create package directory using 'name'
761 if ! test -d "$rootdir${packagedir}/$name"
762 then
763 mkdir -p -- "$rootdir${packagedir}/$name" || chkstatus_or_exit
764 echo "mkdir: created directory '$rootdir${packagedir}/$name'"
767 # Scan for possible conflicts, stop if arise
768 if test "$opt_prune" != opt_prune
769 then
770 echo "Checking for possible conflicts ..."
771 if graft -i -n $graft_r -t "$targetdir" "${packagedir}/$name" 2>&1 | \
772 grep ^CONFLICT
773 then
774 warn "" \
775 " A conflict occurred during installation;" \
776 "Unless the -p option is given, this package won't be LINKED."
777 return 6;
781 # Ignore some signals up to completing the installation
782 trap "" HUP INT QUIT ABRT TERM
784 echo "Decompressing $1 ..."
785 ( cd -- "$rootdir${packagedir}/$name" && tarlz -xf - ) < "$1"
786 chkstatus_or_exit 3
788 # Transite package to Graft control
790 # Remove objects (files, links or directories) from the target
791 # directory that are in conflict with the package directory
792 echo "Pruning any conflict ..."
793 graft -p -D -u $graft_r -t "$targetdir" "${packagedir}/$name"
794 chkstatus_or_exit 2
796 echo "Enabling symbolic links ..."
797 graft -i -P $graft_v $graft_r -t "$targetdir" "${packagedir}/$name"
798 chkstatus_or_exit 2
800 # Avoid unnecessary runs coming from the upgrade_mode(),
801 # this is when the incoming package is **pre-installed**
803 if test "$_isUpgrade" != _isUpgrade.on
804 then
805 # Show package description
806 if test -r "$rootdir${packagedir}/${name}/var/lib/qi/${name}.txt"
807 then
808 awk '/^#/' "$rootdir${packagedir}/${name}/var/lib/qi/${name}.txt"
809 elif test -r "${1}.txt"
810 then
811 # From external meta file (current directory)
812 awk '/^#/' "${1}.txt"
813 else
814 warn "Description file not found for '$name'."
817 # Check and run the post-install script if exist
818 if test -r "$rootdir${packagedir}/${name}/var/lib/qi/${name}.sh"
819 then
820 echo "Running post-install script for \`${name}' ..."
822 # Rely on 'targetdir' if 'rootdir' is empty
823 cd -- "${rootdir:=$targetdir}"/ && \
824 . "$rootdir${packagedir}/${name}/var/lib/qi/${name}.sh"
828 # Check if there are declared packages for replacement
829 if test -r "$rootdir${packagedir}/${name}/var/lib/qi/${name}.replace"
830 then
831 while read -r line
833 for replace in "$rootdir${packagedir}/$(pkgbase $line)"-*
835 if ! test -e "$replace"
836 then
837 warn "Declared package \`${replace}' does not exist. (ignoring)"
838 continue;
841 replace="${replace##*/}"
843 # The search for the package to be replaced cannot
844 # be the same to the incoming package, even to the
845 # temporary location coming from the upgrade_mode()
846 if test "$replace" = "$name" || test "$replace" = "${PRVLOC##*/}"
847 then
848 continue;
851 warn "WARNING: Replacing package \`${replace}' ..."
853 # Since the links belongs to the new package, only
854 # those which are not in conflict can be deleted.
855 # To complete, we will remove the package directory
857 graft -d -D -u $graft_r \
858 -t "$targetdir" "$replace" > /dev/null 2>&1
860 rm -rf -- "$rootdir${packagedir}/$replace"
861 done
862 done < "$rootdir${packagedir}/${name}/var/lib/qi/${name}.replace"
863 unset line
867 # Reset given signals
868 trap - HUP INT QUIT ABRT TERM
870 # Remove used variables
871 unset name
874 resolve_mode() {
875 # Complain if the file cannot be well-read
877 is_readable "$1" || exit 4
879 # Complain if the file does not end in .order
881 if ! fnmatch '*.order' "$1"
882 then
883 warn "\`${1}' does not end in .order"
884 return 4
887 # Get a clean list of the file while printing its contents in reverse
888 # order. The last `awk 'in the pipeline eliminates the non-consecutive
889 # lines, the duplicates. Blank lines, colons and parentheses are
890 # simply ignored, comment lines beginning with '#' are allowed
892 awk \
893 '{ gsub( /:|^#(.*)$|\([^)]*)|^$/,"" ); for( i=NF; i > 0; i-- ) print $i }' \
894 "$1" | awk '!s[$0]++'
897 upgrade_mode()
899 # Complain if the package does not end in .tlz
901 if ! fnmatch '*.tlz' "$1"
902 then
903 warn "\`${1}' does not end in .tlz"
904 return 4
907 # Get the filename
908 incoming="$(basename -- "$1" .tlz)"
910 echo "{^} Upgrading to $incoming ..."
912 # Check package pre-existence
913 if test "$opt_force" != opt_force && \
914 test -e "$rootdir${packagedir}/$incoming"
915 then
916 warn \
917 "" \
918 " The package to be upgraded already exist;" \
919 "Unless the -f option is given, this package won't be UPGRADED."
920 return 6;
923 # Check blacklisted packages
925 echo "Checking blacklist ..."
926 for item in $blacklist
928 case $item in
929 ${incoming}*)
930 warn \
931 "Package name declared on the blacklist \"${incoming}\"." \
932 "" \
933 " This package will be INSTALLED instead of being upgraded." \
935 opt_prune=opt_prune install_mode "$1"
936 return 0
938 esac
939 done
940 unset item
942 # Prepare the package to install it in a temporary location
944 # Set random directory using packagedir as prefix and 'incoming' as suffix
945 PRVLOC=$(mktemp -dp "$rootdir${packagedir}" ${incoming}.XXXXXXXXXXXX) || exit 2
947 # Pre-install the package in the custom 'packagedir'
949 save_packagedir="$rootdir${packagedir}"
950 packagedir="$PRVLOC"
952 echo "Pre-installing package in a temporary location ..."
953 opt_prune=opt_prune # Turn on prune operation.
954 _isUpgrade=_isUpgrade.on install_mode "$1" > /dev/null
955 _isUpgrade=_isUpgrade.off
957 # Restore variable before looking for old packages
958 packagedir=$save_packagedir
959 unset save_packagedir
961 echo "Looking for installations under the same name ..."
962 for long_name in "$rootdir${packagedir}/$(pkgbase $incoming)"*
964 found="${long_name##*/}"
966 # The search for the package to be deleted
967 # cannot be the same to the temporary location
968 test "$long_name" = "$PRVLOC" && continue;
970 fnmatch "$(pkgbase $found)*" "$incoming" || continue;
971 echo "${long_name}: Detected."
973 # A package directory is preserved if -k was given
974 delete_mode $found > /dev/null
975 done
976 unset long_name found
978 # Re-install the package removing the temporary location
980 install_mode "$1"
981 opt_prune=opt_prune.off # Turn off prune operation.
983 echo "Deleting temporary location ..."
984 rm -rf -- "$PRVLOC" || chkstatus_or_exit
985 echo "removed directory: '$PRVLOC'"
987 echo ""
988 echo "Successful upgrade to '${incoming}'."
990 # Remove remaining variables
991 unset incoming PRVLOC
994 warn_mode()
996 # Complain if the package cannot be well-read
998 is_readable "$1" || exit 4
1000 # Complain if the package does not end in .tlz
1002 if ! fnmatch '*.tlz' "$1"
1003 then
1004 warn "\`${1}' does not end in .tlz"
1005 return 4
1008 # List content of files excluding directories
1009 while test -f "$1"
1011 tarlz -tvvf "$1" | awk '!/^drwx/'
1012 chkstatus_or_exit 3
1013 shift;
1014 done
1015 return 0
1018 extract_mode()
1020 # Perform sanity checks before package extraction
1022 is_readable "$1" || exit 4
1024 test -f "$1" || {
1025 warn "\`${1}' is not a regular file."
1026 return 4
1029 # Preparations to extract the package
1031 name="$(basename -- "$1" .tlz)"
1033 # Set random directory using 'name' as prefix
1034 PRVDIR="${TMPDIR}/${name}.${RANDOM-0}$$"
1036 # Trap to remove 'PRVDIR' on disruptions
1037 trap "rm -rf -- $PRVDIR" HUP INT ABRT TERM
1039 # Create 'PRVDIR' removing access for all but user
1040 ( umask 077 ; mkdir -- $PRVDIR )
1041 mkdir -p -m 700 -- $PRVDIR
1043 echo "Extracting package $name ..."
1044 ( umask 000 ; cd -- $PRVDIR && tarlz -xvf - ) < "$1"
1045 if test $? -ne 0
1046 then
1047 # Try to remove (empty) 'PRVDIR' on failure
1048 rmdir -- $PRVDIR
1049 exit 3;
1051 echo "$name has been extracted on $PRVDIR"
1053 # Reset given signals
1054 trap - HUP INT ABRT TERM
1056 # Remove used variables
1057 unset name PRVDIR
1060 ### Extra functions used in the modes
1062 pkgbase()
1064 echo "$(basename -- "$1" .tlz)" | sed -e 's/[-_][.]*[[:digit:]].*$//'
1067 unpack()
1069 for file in "$@"
1071 case $file in
1072 *.tar)
1073 tar -tf "$file" > /dev/null && \
1074 tar -xpf "$file"
1075 chkstatus_or_exit 3
1077 *.tar.gz | *.tgz | *.tar.Z )
1078 gzip -cd "$file" | tar -tf - > /dev/null && \
1079 gzip -cd "$file" | tar -xpf -
1080 chkstatus_or_exit 3
1082 *.tar.bz2 | *.tbz2 | *.tbz )
1083 bzip2 -cd "$file" | tar -tf - > /dev/null && \
1084 bzip2 -cd "$file" | tar -xpf -
1085 chkstatus_or_exit 3
1087 *.tar.lz | *.tlz )
1088 lzip -cd "$file" | tar -tf - > /dev/null && \
1089 lzip -cd "$file" | tar -xpf -
1090 chkstatus_or_exit 3
1092 *.tar.xz | *.txz )
1093 xz -cd "$file" | tar -tf - > /dev/null && \
1094 xz -cd "$file" | tar -xpf -
1095 chkstatus_or_exit 3
1097 *.zip | *.ZIP )
1098 unzip -t "$file" > /dev/null && \
1099 unzip "$file" > /dev/null
1100 chkstatus_or_exit 3
1102 *.gz)
1103 gzip -t "$file" && \
1104 gzip -cd "$file" > "$(basename -- $file .gz)"
1105 chkstatus_or_exit 3
1107 *.Z)
1108 gzip -t "$file" && \
1109 gzip -cd "$file" > "$(basename -- $file .Z)"
1110 chkstatus_or_exit 3
1112 *.bz2)
1113 bzip2 -t "$file" && \
1114 bzip2 -cd "$file" > "$(basename -- $file .bz2)"
1115 chkstatus_or_exit 3
1117 *.lz)
1118 lzip -t "$file" && \
1119 lzip -cd "$file" > "$(basename -- $file .lz)"
1120 chkstatus_or_exit 3
1122 *.xz)
1123 xz -t "$file" && \
1124 xz -cd "$file" > "$(basename -- $file .xz)"
1125 chkstatus_or_exit 3
1128 warn "${PROGRAM}: cannot unpack ${file}: Unsupported extension"
1129 exit 1
1130 esac
1131 done
1132 unset file
1135 do_meta()
1137 # Extract information from the recipe to create the meta file.
1139 # The package description is pre-formatted in 78 columns,
1140 # the '#' character and a space is added as prefix to conform
1141 # the 80 columns in total
1143 cat << EOF
1144 $(echo "$description" | fold -w 78 | awk '$0="# " $0')
1146 QICFLAGS="$QICFLAGS"
1147 QICXXFLAGS="$QICXXFLAGS"
1148 QILDFLAGS="$QILDFLAGS"
1149 pkgname=$pkgname
1150 pkgversion=$pkgversion
1151 arch=$arch
1152 release=$release
1153 pkgcategory=$pkgcategory
1154 full_pkgname=$full_pkgname
1155 blurb="$(echo "$description" | sed -e '/^$/d;2q')"
1156 homepage="$homepage"
1157 license="$license"
1158 fetch="$fetch"
1159 replace="$replace"
1164 ### Default values
1166 PROGRAM="${0##*/}"
1167 packagedir=@PACKAGEDIR@
1168 targetdir=@TARGETDIR@
1169 blacklist="perl5 graft tarlz plzip musl glibc"
1170 RC=RC
1171 RCFILE=@SYSCONFDIR@/qirc
1172 opt_install=opt_install.off
1173 opt_upgrade=opt_upgrade.off
1174 opt_force=opt_force.off
1175 opt_keep=opt_keep.off
1176 opt_incr_release=opt_incr_release.off
1177 opt_skipqsts=opt_skipqsts.off
1178 opt_nopkg=opt_nopkg.off
1179 opt_prune=opt_prune.off
1180 rootdir=""
1181 jobs=1
1182 mode=""
1183 verbose=0
1184 graft_v=-v
1185 graft_r=""
1186 _isUpgrade=_isUpgrade.off
1187 TMPDIR="${TMPDIR:=/usr/src/qi/build}"
1188 QICFLAGS="${QICFLAGS:=-g0 -Os}"
1189 QICXXFLAGS="${QICXXFLAGS:=$QICFLAGS}"
1190 QILDFLAGS="${QILDFLAGS:=-s}"
1191 worktree=/usr/src/qi
1192 tardir=${worktree}/sources
1193 outdir=/var/cache/qi/packages
1194 netget="wget -c -w1 -t3 --no-check-certificate"
1195 rsync="rsync -v -a -L -z -i --progress"
1196 configure_args="--prefix=@PREFIX@ --libexecdir=@LIBEXECDIR@ --bindir=@BINDIR@ --sbindir=@SBINDIR@ --sysconfdir=@SYSCONFDIR@ --localstatedir=@LOCALSTATEDIR@"
1197 infodir=@INFODIR@
1198 mandir=@MANDIR@
1199 docdir=@DOCDIR@
1201 # Store (default) directory locations
1202 QI_TARGETDIR=$targetdir
1203 QI_PACKAGEDIR=$packagedir
1204 QI_WORKTREE=$worktree
1205 QI_TARDIR=$tardir
1206 QI_OUTDIR=$outdir
1208 ### Parse options
1210 while getopts :bcdiouwxLNP:t:fkvO:W:Z:a:j:1nSpr:hV name
1212 case $name in
1214 if test -z "$mode"
1215 then
1216 readconfig
1217 mode=build_mode
1221 mode=create_mode
1224 readconfig
1225 mode=delete_mode
1228 if test -z "$mode"
1229 then
1230 readconfig
1231 mode=install_mode
1233 if test "$mode" = build_mode
1234 then
1235 opt_install=opt_install
1239 mode=resolve_mode
1242 if test -z "$mode"
1243 then
1244 readconfig
1245 mode=upgrade_mode
1247 if test "$mode" = build_mode
1248 then
1249 opt_upgrade=opt_upgrade
1253 mode=warn_mode
1256 mode=extract_mode
1259 printf "%s\n" \
1260 "QI_TARGETDIR=$QI_TARGETDIR" \
1261 "QI_PACKAGEDIR=$QI_PACKAGEDIR" \
1262 "QI_WORKTREE=$QI_WORKTREE" \
1263 "QI_TARDIR=$QI_TARDIR" \
1264 "QI_OUTDIR=$QI_OUTDIR"
1265 exit 0
1268 RC=RC.off
1271 packagedir="$OPTARG"
1274 targetdir="$OPTARG"
1277 opt_force=opt_force
1280 opt_keep=opt_keep
1283 verbose=$(( verbose + 1 ))
1286 outdir="$OPTARG"
1289 worktree="$OPTARG"
1292 tardir="$OPTARG"
1295 arch="$OPTARG"
1298 jobs="$OPTARG"
1301 opt_incr_release=opt_incr_release
1304 opt_nopkg=opt_nopkg
1307 opt_skipqsts=opt_skipqsts
1310 opt_prune=opt_prune
1313 rootdir="$OPTARG"
1316 usage
1317 exit 0
1320 version
1321 exit 0
1324 warn "Option '-${OPTARG}' requires an argument"
1325 usage
1326 exit 1
1329 warn "Illegal option -- '-${OPTARG}'"
1330 usage
1331 exit 1
1333 esac
1334 done
1335 shift $(( OPTIND - 1 ))
1337 if test $# -eq 0
1338 then
1339 usage
1340 exit 1
1343 # Program sanity check
1345 unset -f command; # Ensure command is not a user function.
1347 for need in awk basename chmod cp dirname find fold graft grep \
1348 mkdir mktemp rm rmdir sed sha256sum stat tarlz
1350 if ! command -v $need > /dev/null
1351 then
1352 warn "${PROGRAM}: Prerequisite \`${need}' not found in PATH"
1353 exit 2
1355 done
1356 unset need
1358 # Determine verbosity level/flag
1360 if test "$verbose" -gt 1
1361 then
1362 graft_v=-V
1365 # Read standard input if FILE is -, or when
1366 # FILE is not connected to a terminal.
1368 if test "$1" = - || test ! -t 0
1369 then
1370 # Unset positional parameters setting $# to zero
1371 set --
1373 # Assign remaining arguments to the positional parameters
1374 while read -r input
1376 set -- "$@" "$input"
1377 done
1380 # We need at least one operating mode
1381 if test -z "$mode"
1382 then
1383 usage
1384 exit 4
1387 # Validate 'packagedir' and 'targetdir' as canonical directories
1389 # The single slash '/' does not qualify here
1390 if ! fnmatch '/?*' "$packagedir"
1391 then
1392 warn "${PROGRAM}: Package directory \`${packagedir}' is not fully qualified"
1393 exit 4
1395 if test ! -d "$packagedir"
1396 then
1397 warn "${PROGRAM}: Package directory \`${packagedir}' does not exist"
1398 exit 4
1401 # The single slash '/' is valid here
1402 if ! fnmatch '/*' "$targetdir"
1403 then
1404 warn "${PROGRAM}: Target directory \`${targetdir}' is not fully qualified"
1405 exit 4
1407 if test ! -d "$targetdir"
1408 then
1409 warn "${PROGRAM}: Target directory \`${targetdir}' does not exist"
1410 exit 4
1413 # Validate 'rootdir' directory
1415 if test -n "$rootdir"
1416 then
1417 if test -d "$rootdir" && test "$rootdir" != /
1418 then
1419 # Remove slash from the end
1420 rootdir="${rootdir%/}"
1422 # A workaround for graft-2.13+. The specified directory is
1423 # relative to the log file, we prepend it inside the rootdir
1425 eval "$(graft -L)" ; GRAFT_LOGFILE="${GRAFT_LOGFILE:=/var/log/graft}"
1426 mkdir -p -- "$rootdir$(dirname -- $GRAFT_LOGFILE)" || chkstatus_or_exit
1428 # Compose 'rootdir' and log file option to be used with graft(1)
1429 graft_r="-r $rootdir -l $GRAFT_LOGFILE"
1431 # Unset variables coming from eval
1432 unset GRAFT_PERL GRAFT_LOGFILE GRAFT_TARGETDIR GRAFT_PACKAGEDIR
1433 else
1434 warn "${PROGRAM}: Root directory \`${rootdir}' is not fully qualified"
1435 exit 4
1437 export rootdir
1440 # Ensure 'TMPDIR' creation to prefix temporary files
1442 if test ! -d "$TMPDIR"
1443 then
1444 mkdir -p -- "$TMPDIR" || chkstatus_or_exit
1446 readonly TMPDIR
1448 # Process each package or recipe provided on the command-line
1450 for package in "$@"
1452 $mode $package
1453 done