source: BLFS/libs/func_dependencies@ 50cf934

ablfs-more legacy trunk
Last change on this file since 50cf934 was e030049, checked in by Pierre Labastie <pierre@…>, 7 years ago

BLFS/func_dependencies: when building the graph, and DEP_LEVEL=3 (build opt
deps only for requested packages), it may happen that a requested packages
is seen at depth > 2 before it is seen at depth=2. In this case, optional
deps are not processed the first time, and then the package is not processed
at depth 2 because it has already been seen. Add a case statement against
that.

  • Property mode set to 100644
File size: 19.9 KB
Line 
1#!/bin/bash
2#
3# $Id$
4#
5
6#-----------------------------------------------------------------------------#
7# This is a set of (recursive) functions for manipulating a dependency graph. #
8# We use algorithms and definitions from chapter 4 (mainly section 4.2) of #
9# https://algs4.cs.princeton.edu/. The graph we manipulate is the directed #
10# graph of the dependencies: nodes are packages in the BLFS book. A node A is #
11# connected to a node B if package A depends on B. A topological order (rev- #
12# erted) is exactly what we want for a build order. But a topological order #
13# only exists if the graph is acyclic. We'll therefore have to remove cycles. #
14# There are a number of other features we want to consider: #
15# - edges are weighted according to the dependency requirement: #
16# 1 for required #
17# 2 for recommended #
18# 3 for optional #
19# 4 for external #
20# We should consider only edges with weight lower or equal to that #
21# specified by the user, but see below. #
22# - we do not want to build the whole book. The user specifies a set of #
23# packages, and we have to consider only nodes reachable from this set #
24# using edges of weight not exceeding the specified weight. #
25# - when doing the topological sort, we want to consider all the edges and #
26# not only those not exceeding the specified weight: If a package A in the #
27# reachable subgraph depends optionally on another package B in the same #
28# subgraph, we want to build B before A. But this means we'll have to #
29# remove cycles for all weights. #
30# - dependencies have another qualifier: before or after. The problem: if a #
31# package A depends on B with an "after" qualifier, and a package C depends #
32# on A with a "before" qualifier, C may need B to be able to use A. So the #
33# only safe way to consider "after" qualifiers is to consider that they are #
34# "before" deps for any parent of the packages considered. #
35# We'll therefore have a 3 pass procedure. First build the set of nodes #
36# reachable from the root set. Second, remove dangling edges (those pointing #
37# to packages outside the node set), and move "after" edges to "before" edges #
38# originating from the parents. Third remove cycles and generate a #
39# topological sort. #
40# #
41# TODO: document each pass
42# Data layout: #
43# TODO: needs also to document the .tree files, and the "f" qualifier
44# #
45# A node of the tree is represented by a text file <nodeName>.dep. Each edge #
46# starting from this node is represented by a line in this file. We keep #
47# those files in the same directory. We introduce a special node named root, #
48# whose edges point to the list of nodes requested by the user. Each line #
49# contains three fields: #
50# - the weight of the edge #
51# - the "before" (b) or "after" (a) qualifier #
52# - the name of the destination of the edge #
53# #
54# TODO: The following is obsolete
55# Circular dependencies: #
56# #
57# In case we find a cirdular dependency, it has the form : #
58# parent->dependency_0->...->dependency_n->dependency_0 #
59# If we want to build dependency_n before dependency_0, no problem: #
60# we just prune the tree at dependency_n. If we want to build first #
61# dependency_0, we need to put dependency_n as a dependency of parent, #
62# then erase and rebuild the subtree from there. Now, we may have met #
63# another circular dependency in the subtree, and erasing the tree makes #
64# us forget the decision which was made. So, after first generating the #
65# list of dependencies from packages.xml, we keep the generated list in #
66# a file <nodeName>.odep, which we modify according to the decision which #
67# was made. #
68#---------------------------------------------------------------------------#
69
70# Global variables:
71# A string of spaces for indenting:
72declare -a spaceSTR=" "
73# When we are backing up from a circular dependency, `parentNode'
74# contains the node which has an edge entering the cycle
75declare parentNode
76
77#---------------------#
78generate_subgraph() { #
79#---------------------#
80: <<inline_doc
81 function: Create a subgraph of all the nodes reachable from the node
82 represented by the file whose name is $1. The edges considered
83 are those with maximal weight DEP_LEVEL (recursive function).
84 input vars: $1 : file name corresponding to the node whose edges will be
85 followed for the DFS
86 $2 : weight of the edge leading to this node
87 $3 : depth (root is 1)
88 $4 : qualifier (a for after, b for before)
89 externals: vars: DEP_LEVEL contains 1 if we want to build the
90 tree only for required dependencies,
91 2 if we want also recommended ones,
92 3 if we want also optional ones, but only
93 for the requested packages,
94 4 if we want all the dependencies
95 MAIL_SERVER contains the name of the MTA we want to use.
96 files: ../xsl/dependencies.xsl: stylesheet for creating the
97 .dep files
98 ../packages.xml: File containing packages id
99 and dependencies
100 returns: 0 if the tree has been successfully created
101 output: files: for each node reachable from $1, a file <node>.dep.
102 on error: nothing
103 on success: nothing
104inline_doc
105
106local depFile=$1
107local -i weight=$2
108local -i depth=$3
109local qualifier=$4
110local -i spacing=0
111local priostring
112local buildstring
113local id_of_dep
114local prio_of_dep
115local build_of_dep
116local dep_level
117
118if (( depth < 10 )); then spacing=1; fi
119case $weight in
120 1) priostring=required ;;
121 2) priostring=recommended ;;
122 3) priostring=optional ;;
123esac
124case $qualifier in
125 a) buildstring=runtime ;;
126 b) buildstring= ;;
127esac
128dep_level=$DEP_LEVEL
129if [ "$dep_level" = 3 ] && [ "$depth" -gt 2 ]; then dep_level=2; fi
130if [ "$dep_level" -gt 3 ]; then dep_level=3; fi
131echo -en "\nNode: $depth${spaceSTR:0:$(( depth + spacing ))}${RED}${depFile%.dep}${OFF} $priostring $buildstring"
132
133depth=$(( depth + 1 ))
134if (( depth < 10 )); then spacing=1; else spacing=0; fi
135# Start of loop
136{
137while read prio_of_dep build_of_dep id_of_dep; do
138 case $prio_of_dep in
139 1) priostring=required ;;
140 2) priostring=recommended ;;
141 3) priostring=optional ;;
142 4) priostring=external ;;
143 esac
144 case $build_of_dep in
145 a) buildstring=runtime ;;
146 b) buildstring= ;;
147 esac
148# Has this entry already been seen?
149# We have a special case here: if the entry has been seen at depth > 2
150# and now depth=2 and DEP_LEVEL=3, optional deps have not been processed.
151# If this is the case, just consider it has not been seen.
152 if [ -f ${id_of_dep}.dep ] ; then
153 case $depth$DEP_LEVEL in
154 23) ;;
155 *)
156# Just display it and proceed.
157 echo -en "\nEdge: $depth${spaceSTR:0:$((depth + spacing))}${MAGENTA}${id_of_dep}${OFF} $priostring $buildstring"
158 continue
159 ;;
160 esac
161 fi
162# Is the weight higher than requested?
163 if [ "$prio_of_dep" -gt $dep_level ]; then
164# Just display it and proceed.
165 echo -en "\n Out: $depth${spaceSTR:0:$((depth + spacing))}${YELLOW}${id_of_dep}${OFF} $priostring $buildstring"
166 continue
167 fi
168# Otherwise, let's build the corresponding subgraph.
169 xsltproc --stringparam idofdep "$id_of_dep" \
170 --stringparam MTA "$MAIL_SERVER" \
171 -o ${id_of_dep}.dep \
172 ../xsl/dependencies.xsl ../packages.xml
173
174 if [[ -s ${id_of_dep}.dep ]]; then # this dependency has dependencies
175 generate_subgraph ${id_of_dep}.dep $prio_of_dep $depth $build_of_dep
176 else # id_of_dep has no dependencies, just touch the file and display
177 touch ${id_of_dep}.dep
178 echo -en "\nLeaf: $depth${spaceSTR:0:$((depth + spacing))}${CYAN}${id_of_dep}${OFF} $priostring $buildstring"
179 fi
180done
181} <$depFile
182depth=$(( depth - 1 ))
183if (( depth < 10 )); then spacing=1; else spacing=0; fi
184echo -en "\n End: $depth${spaceSTR:0:$((depth + spacing))}${GREEN}${depFile%.dep}${OFF}"
185return 0
186}
187
188#-----------#
189path_to() { #
190#-----------#
191: <<inline_doc
192 function: check whether there is a path from $1 to $2 on the graph
193 input vars: $1 contains the filename of the starting node.
194 $2 contains the name of the node to reach
195 $3 contains the weight above which we do not want to
196 follow an edge
197 "seen" (global) contains the list of already seen nodes
198 returns: 0 if the node has been found
199 1 if not
200 on error: nothing
201 on success: nothing
202inline_doc
203local start=$1
204local seek=$2
205local prio=$3
206local prio_of_dep
207local build_of_dep
208local id_of_dep
209local r
210
211if test "${start%.dep}" = "$seek"; then return 0; fi
212seen="$seen${start%.dep} "
213if test -s $start; then
214 {
215 while read prio_of_dep build_of_dep id_of_dep; do
216 if test "$prio" -lt "$prio_of_dep"; then continue; fi
217 if ! test "${seen% $id_of_dep *}" = "$seen"; then continue; fi
218 if path_to ${id_of_dep}.dep $seek $prio; then return 0; fi
219 done
220 } < $start
221fi
222return 1
223}
224#------------------#
225clean_subgraph() { #
226#------------------#
227: <<inline_doc
228 function: Remove dangling edges and create groups of deps for "after"
229 deps: A-before->B-after->C becomes:
230 A -before-> Bgroupxx -before-> B
231 \
232 -before-> C
233 the name of the group is chosen so that it is unlikely as
234 a package name (so that it is removed when building the
235 xml book).
236 input vars: None
237 files: <node>.dep files containing dangling edges and
238 "after" qualifiers
239 returns: 0
240 output: files: <node>.dep files containing no dangling edges and
241 no "after" qualifiers
242 on error: nothing
243 on success: nothing
244inline_doc
245
246local node
247local id_of_dep
248local prio_of_dep
249local build_of_dep
250local lines_to_remove
251local lines_to_change
252local parent
253local p
254local b
255local start
256local seen
257
258for node in $(ls *.dep); do
259 if test $node = root.dep; then continue; fi
260 echo Cleaning $node
261 lines_to_remove=
262 {
263 while read prio_of_dep build_of_dep id_of_dep; do
264 if ! test -f ${id_of_dep}.dep; then
265 lines_to_remove="$lines_to_remove $id_of_dep"
266 continue
267 fi
268 done
269 } <$node
270 for id_of_dep in $lines_to_remove; do
271 sed "/\ $id_of_dep\$/d" -i $node
272 done
273done
274for node in $(grep -l ' a ' *.dep); do
275 lines_to_remove=
276 echo Process "runtime" deps in $node
277 if ! [ -e ${node%.dep}groupxx.dep ]; then
278 b=0
279 for parent in $(grep -l ${node%.dep}\$ *); do
280 p=0
281 for start in $(grep ' a ' $node | cut -d' ' -f3); do
282 seen=" "
283 if path_to ${start}.dep ${parent%.dep} 3; then p=1; break; fi
284 done
285 if test $p = 0; then
286 b=1
287 sed -i "s/\ ${node%.dep}\$/&groupxx/" $parent
288 fi
289 done
290 echo "1 b ${node%.dep}" > ${node%.dep}groupxx.dep
291 if test $b = 0; then echo "1 b ${node%.dep}groupxx" >> root.dep; fi
292 fi
293 {
294 while read prio_of_dep build_of_dep id_of_dep; do
295 if test $build_of_dep = a; then
296 if ! grep -q ${id_of_dep} ${node%.dep}groupxx.dep; then
297 echo "$prio_of_dep b ${id_of_dep}" >> ${node%.dep}groupxx.dep
298 fi
299 lines_to_remove="$lines_to_remove $id_of_dep"
300 fi
301 done
302 } <$node
303 for id_of_dep in $lines_to_remove; do
304 sed "/\ $id_of_dep\$/d" -i $node
305 done
306done
307for node in $(grep -l ' f ' *); do
308 echo Process "first" deps in $node
309 lines_to_change=
310 {
311 while read prio_of_dep build_of_dep id_of_dep; do
312 if test $build_of_dep = f; then
313 if ! test -f ${id_of_dep}-pass1.dep; then
314 cp ${id_of_dep}{,-pass1}.dep;
315 fi
316 lines_to_change="$lines_to_change $id_of_dep"
317 unset lr
318 {
319 while read p b start; do
320 seen=" "
321 if path_to ${start}.dep ${node%.dep} $p; then
322 lr="$lr $start"
323 fi
324 done
325 } < ${id_of_dep}-pass1.dep
326 for p in $lr; do
327 sed "/\ $p\$/d" -i ${id_of_dep}-pass1.dep
328 done
329 fi
330 done
331 } <$node
332 for id_of_dep in $lines_to_change; do
333 sed "/\ $id_of_dep\$/"'{s/[[:digit:]] f /1 b /;s/$/-pass1/}' -i $node
334 done
335done
336} # End clean_subgraph
337
338#----------------------------#
339generate_dependency_tree() { #
340#----------------------------#
341: <<inline_doc
342 function: Create a subtree of the dependency tree
343 (recursive function)
344 input vars: $1 : file with a list of targets (child nodes)
345 the first line of the file is the link
346 $2 : priority (1=req, 2=rec, 3=opt)
347 returns: 0 if the tree has been successfully created
348 1 if we are backing up to the parent of a circular dep
349 and there are only required deps in the cycle
350 2 if we are backing up to the parent of a circular dep
351 and there are recommended deps and no optional deps in the
352 cycle
353 3 if we are backing up to the parent of a circular dep
354 and there are optiional deps in the cycle
355 modifies: vars: ParentNode is set when return is not 0
356 output: files: for each <pkg> with dependencies in $1,
357 a file <pkg>.tree and its dependencies
358 on error: nothing
359 on success: nothing
360inline_doc
361
362local depFile=$1
363local priority=$2
364local -a rootlink
365local -a priolink
366local -a otherlink
367local -i depth
368local -i count=0
369local id_of_dep
370local build_of_dep
371local prio_of_dep
372local parent
373local lines_to_remove=
374local srootlink
375local priostring
376local dpriostring
377local i
378
379{
380read -a rootlink
381depth=${#rootlink[*]}
382read -a priolink
383srootlink="${rootlink[*]} "
384case $priority in
385 1) priostring=required ;;
386 2) priostring=recommended ;;
387 3) priostring=optional ;;
388esac
389# start of depFile
390echo -en "\nNode: $depth${spaceSTR:0:$depth}${RED}${depFile%.tree}${OFF} $priostring"
391
392while read prio_of_dep build_of_dep id_of_dep; do
393 case $prio_of_dep in
394 1) dpriostring=required ;;
395 2) dpriostring=recommended ;;
396 3) dpriostring=optional ;;
397 esac
398# count entries in file
399 (( count++ ))
400# Has this entry already been seen?
401 if [ -f ${id_of_dep}.tree ]; then # found ${id_of_dep}.tree already in tree
402 otherlink=($(head -n 1 ${id_of_dep}.tree))
403 if [ -z "${otherlink[*]}" ]
404 then echo otherlink empty for $id_of_dep.tree
405 echo This should not happen, but happens to happen...
406 exit 1
407 fi
408#Do not use "${rootlink[*]}" =~ "${otherlink[*]}": case rootlink=(1 11)
409# and otherlink=(1 1)
410 if [[ ${srootlink#"${otherlink[*]} "} != ${srootlink} ]]; then # cir. dep
411 echo -en "\nCirc: $((depth+1))${spaceSTR:0:$((depth+1))}${YELLOW}${id_of_dep}${OFF} $dpriostring"
412# Find lowest priority in branch from parent to depFile:
413 p2=0
414 for (( i=${#otherlink[*]}; i < $depth ; i++ )) ; do
415 if (( ${priolink[i]} > $p2 )); then p2=${priolink[i]}; fi
416 done
417 if (( $prio_of_dep >= $p2 )); then # prune
418 lines_to_remove="$lines_to_remove $id_of_dep"
419 sed -i "/$id_of_dep/d" ${depFile/.tree/.dep}
420 else # find and set parent, then return lowest priority
421# The parent has the same link without the last entry.
422# We do not need otherlink anymore so just destroy the last element
423 unset otherlink[-1]
424 parentNode=$(grep ^"${otherlink[*]}"\$ -l *)
425 return $p2
426 fi
427 else # not circular: prune tree (but not .dep, since it may happen that
428 # the tree is destroyed and rebuilt in another order)
429 lines_to_remove="$lines_to_remove $id_of_dep"
430 fi # circular or not
431 continue # this dependency has already been seen, and the associated
432 # subtree computed. We are done
433 fi # Has this entry already been seen?
434# So, this entry has not already been seen. Let's build the corresponding
435# subtree. First check there is a subtree...
436# Use -s, because it may happen that after removing lines, .dep exists
437# but is empty.
438 if [[ -s ${id_of_dep}.dep ]]; then # this dependency has dependencies
439 sed "1i${rootlink[*]} $count\\
440${priolink[*]} $prio_of_dep" < ${id_of_dep}.dep \
441 > ${id_of_dep}.tree # add link and priolink
442 generate_dependency_tree ${id_of_dep}.tree $prio_of_dep
443# Test return value, in case we exchange dependencies
444 p2=$?
445 case $p2 in
446 0) # Normal return
447 ;;
448 $prio_of_dep) # we remove this dep, but since it may become unreachable,
449 # move it to be built later (as a dep of parent).
450 tree_erase ${id_of_dep}.tree
451 lines_to_remove="$lines_to_remove $id_of_dep"
452 sed -i "/${id_of_dep}/d" ${depFile/.tree/.dep}
453 echo "$prio_of_dep b $id_of_dep" >> $parentNode
454# must be added to .dep in case parentNode is destroyed when erasing
455# the tree
456 echo "$prio_of_dep b $id_of_dep" >> ${parentNode/.tree/.dep}
457 continue
458 ;;
459 *) # We are backing up
460 return $p2
461 ;;
462 esac
463 else # id_of_dep has no dependencies, just record the link in a file
464 # and print
465 echo "${rootlink[*]} $count" > ${id_of_dep}.tree
466 echo -en "\nLeaf: $(($depth+1))${spaceSTR:0:$(($depth+1))}${CYAN}${id_of_dep}${OFF} $dpriostring"
467 fi
468done
469echo -en "\n End: $depth${spaceSTR:0:$depth}${GREEN}${depFile%.tree}${OFF}"
470} <$depFile
471# It may happen that a file is created with several times
472# the same line. Normally, all those lines but one
473# would be flagged to be removed (or all of them if
474# the dependency appeared before). A simple sed /$line/d
475# destroys all the lines. We should instead remove
476# only one for each appearance of it in lines_to_remove.
477# so first get the position of last line and then delete
478# that line
479for line in $lines_to_remove
480 do lineno=$(sed -n /^[[:digit:]]\ b\ $line\$/= $depFile | tail -n1)
481 sed -i ${lineno}d $depFile
482done
483return 0
484}
485
486
487#---------------#
488tree_browse() { #
489#---------------#
490local file=$1
491local f
492
493#echo file=$file
494for f in $(grep '[^0-9 ]' $file | sed 's/.* //'); do
495# echo f=$f
496 if grep -q '[^0-9 ]' ${f}.tree ; then
497 tree_browse ${f}.tree
498 fi
499 echo $f
500done
501}
502
503#--------------#
504tree_erase() { #
505#--------------#
506local file=$1
507local f
508local rootlink
509local rootlink2
510
511#echo file=$file
512rootlink="$(head -n1 $file) "
513for f in $(grep '[^0-9 ]' $file | sed 's/.* //'); do
514 if [ -f ${f}.tree ]; then
515 rootlink2="$(head -n1 ${f}.tree) "
516# We want two things:
517# i) do not erase the file if it is in another branch
518# ii) do not erase the file if there is a circular dependency
519# for case i), we test that rootlink is contained in rootlink2
520# for case ii), we test that rootlink2 is not contained in
521# rootlink.
522# See comment above about srootlink
523 if [[ ${rootlink2#${rootlink}} != ${rootlink2} &&
524 ${rootlink#${rootlink2}} == ${rootlink} ]] ; then
525 tree_erase ${f}.tree
526 fi
527 fi
528done
529rm -f $file
530}
Note: See TracBrowser for help on using the repository browser.