This repository has been archived on 2024-10-17. You can view files and clone it, but cannot push or open issues or pull requests.
MahiroOS-jhalfs/BLFS/libs/func_dependencies
Pierre Labastie 5156f51859 BLFS: remove only "a" lines for runtime deps
Presently, when the <node>groupxx file is created, we copy "a"
lines from <node> to <node>groupxx (changing "a" to "b") and
we remove all the lines containing the runtime dependency from
<node>. This may not always be what we want: for example, a
dependency can be both "required at runtime" and "optional for
tests". We should keep this information when cleaning the nodes.
Only when removing circular paths should the optional tie be broken
if necessary.
2022-09-07 15:51:45 +02:00

631 lines
28 KiB
Bash

#!/bin/bash
#
#-----------------------------------------------------------------------------#
# This is a set of (recursive) functions for manipulating a dependency graph. #
# We use algorithms and definitions from chapter 4 (mainly section 4.2) of #
# https://algs4.cs.princeton.edu/. The graph we manipulate is the directed #
# graph of the dependencies: nodes are packages in the BLFS book. A node A is #
# connected to a node B if package A depends on B. A topological order (rev- #
# erted) is exactly what we want for a build order. But a topological order #
# only exists if the graph is acyclic. We'll therefore have to remove cycles. #
# There are a number of other features we want to consider: #
# - edges are weighted according to the dependency requirement: #
# 1 for required #
# 2 for recommended #
# 3 for optional #
# 4 for external #
# We should consider only edges with weight lower or equal to that #
# specified by the user, but see below. #
# - we do not want to build the whole book. The user requests a set of #
# packages, and we'd like to consider only nodes reachable from this set #
# using edges of weight not exceeding the specified weight. #
# - we do not want to rebuild packages already built. But we still have to #
# generate the full dependency graph, because if A depends on B, which is #
# already built, and B depends on C, which is not built, or needs to be #
# updated, then A may depends on C. We therefore have to remove already #
# built (and up to date) packages from the graph, but need to keep the #
# dependency chain. #
# - when doing the topological sort, we want to consider all the edges and #
# not only those not exceeding the specified weight: If a package A in the #
# reachable subgraph depends optionally on another package B in the same #
# subgraph, we want to build B before A if possible. But this means we'll #
# have to remove cycles for all weights. #
# - dependencies have another qualifier: before, after, or first. We use #
# it as follows: for "after", we can build the dependency after the #
# package, but if a package A depends on B with an "after" qualifier, and #
# a package C depends on A with a "before" qualifier, C may need B to be #
# able to use A. So the only safe way to consider "after" qualifiers is to #
# consider that they are "before" deps for any parent of the packages #
# considered. There is an exception to that rule: if B depends on C #
# (possibly through a chain of several dependencies), then C should still #
# be built before B. For "after", the dependency has to be built both #
# before and after the package. So we duplicate the dependency as a #
# "-pass1" package, and change the graph accordingly. #
# We'll therefore have a 3 pass procedure. First build the set of nodes #
# reachable from the root set. Second, remove dangling edges (those pointing #
# to packages outside the node set), and move "after" edges to "before" edges #
# originating from the parents as well as creating the "-pass1" nodes. Third #
# remove cycles and generate a topological sort. #
# #
# Pass 1: graph generation #
# ======================== #
# Data layout for pass 1 #
# ---------------------- #
# A node of the graph is represented by a text file <nodeName>.dep. Each edge #
# starting from this node is represented by a line in this file. We keep #
# those files in the same directory. We introduce a special node named root, #
# whose edges point to the list of nodes requested by the user. Each line #
# contains three fields: #
# - the weight of the edge #
# - the qualifier: "before" (b), "after" (a), or "first" (f) #
# - the name of the destination of the edge (without the ".dep" extension) #
# #
# Recursive function "generate_subgraph" #
# -------------------------------------- #
# This function treats a node of the graph that is not a leaf and that is #
# seen for the first time in the DFS. The dependencies of this node are #
# known, and stored in a .dep file. For each dependency in that file, there #
# are three cases: #
# - the weight of the edge leading to that dependency is higher than #
# requested. This dependency is discarded (some information printed) #
# - the weight of the edge is lower or equal to requested, but the node #
# has already been visited (the .dep file exists). Discard too (some #
# information printed) #
# - the weight of the edge is lower or equal to requested, and the node #
# has not been seen: then the dependencies of that node are generated, #
# and there are two cases: #
# - the node has no dependencies: just create an empty .dep file, so #
# that we know the node has been visited #
# - the node has dependencies: call generate_subgraph for that node #
# #
# This function takes four parameters: #
# - The node filename: this is the only one useful for the algorithm #
# - The depth: number of steps starting from root (for pretty print only) #
# - The weight of the edge leading to that node (for printing) #
# - The qualifier (for printing) #
# #
# Pass 2: graph transformation #
# ============================ #
# We now have three loops over nodes of the graph #
# Loop 1: Remove dead edges #
# ------------------------- #
# Since some nodes have not been created because the edges leading to them #
# had too high a weight, those edges have to be suppressed. #
# For each existing node file, we make a list of lines to remove by #
# testing whether the destination exists. We then remove the lines. #
# Another approach would be to make a temporary file and output only #
# lines that should stay, then rename the file. This would save a loop. #
# All in all it is an N*e process, where N is the number of nodes and e #
# the average number of edges originating from a node. #
# Loop 2: Treat "after" edges #
# --------------------------- #
# If a node is the origin of edges qualified as "after", we want the #
# nodes which are the destination of those edges to be built after #
# the origin node, but before any node that depend on the origin #
# node. For that, the general rule is to change: #
# P---b--->A---a--->D #
# to: #
# P---b--->Agroupxx---b--->A #
# | #
# ---b--->D #
# But there is a problem if D depends on P, possibly through a chain, #
# because we create a cycle which shouldn't exist. If this is the case, #
# we leave A as a dependency of P: #
# P---b--->A #
# #
# Agroupxx---b--->A #
# | #
# ---b--->D #
# Doing so, it may happen that Agroupxx has no parent. We then add #
# Agroupxx as a dependency of root. The problem with this algorithm is #
# the search for paths from D to A, which may be exponential in the #
# number of nodes in the graph. #
# #
# Loop 3: Add -pass1 nodes #
# ------------------------ #
# Sometimes there is no way to escape a cycle. A package A needs B, and B #
# needs A. In that case, it is often possible to build a degraded version #
# of package A, then B, then rebuild A. The book indicates this with the #
# following dependency chain, using a qualifier of "first": #
# B---f--->A---b--->X...Y---b--->B #
# where the X...Y notation represents a chain of dependencies from A to B. #
# So the third loop is over nodes containing "f" qualifiers, and does the #
# following: it creates a new node A-pass1, which is a copy of A, and #
# remove from A-pass1 all the dependencies leading to B through a chain, #
# to obtain: #
# A---b--->X...Y---b--->B---b--->A-pass1 #
# It may then happen that nothing depends on A. So this is tested, and A #
# is added to the root node if it is orphaned. #
# TODO: document the third pass #
# TODO: needs also to document the .tree files #
# TODO: The following is obsolete #
# Circular dependencies: #
# #
# In case we find a cirdular dependency, it has the form : #
# parent->dependency_0->...->dependency_n->dependency_0 #
# If we want to build dependency_n before dependency_0, no problem: #
# we just prune the tree at dependency_n. If we want to build first #
# dependency_0, we need to put dependency_n as a dependency of parent, #
# then erase and rebuild the subtree from there. Now, we may have met #
# another circular dependency in the subtree, and erasing the tree makes #
# us forget the decision which was made. So, after first generating the #
# list of dependencies from packages.xml, we keep the generated list in #
# a file <nodeName>.odep, which we modify according to the decision which #
# was made. #
#---------------------------------------------------------------------------#
# Global variables:
# A string of spaces for indenting:
declare -a spaceSTR=" "
# When we are backing up from a circular dependency, `parentNode'
# contains the node which has an edge entering the cycle
declare parentNode
#---------------------#
generate_subgraph() { #
#---------------------#
: <<inline_doc
function: Create a subgraph of all the nodes reachable from the node
represented by the file whose name is $1. The edges considered
are those with maximal weight DEP_LEVEL (recursive function).
input vars: $1 : file name corresponding to the node whose edges will be
followed for the DFS
$2 : weight of the edge leading to this node
$3 : depth (root is 1)
$4 : qualifier (a for after, b for before, f for first)
externals: vars: DEP_LEVEL contains 1 if we want to build the
tree only for required dependencies,
2 if we want also recommended ones,
3 if we want also optional ones, but only
for the requested packages,
4 if we want all the dependencies
(excluding external of course)
MAIL_SERVER contains the name of the MTA we want to use.
files: ../xsl/dependencies.xsl: stylesheet for creating the
.dep files
../packages.xml: File containing packages id
and dependencies
returns: 0 if the tree has been successfully created
output: files: for each node reachable from $1, a file <node>.dep.
on error: nothing
on success: nothing
inline_doc
local depFile=$1
local -i weight=$2
local -i depth=$3
local qualifier=$4
local -i spacing=0
local priostring
local buildstring
local id_of_dep
local prio_of_dep
local build_of_dep
local dep_level
if (( depth < 10 )); then spacing=1; fi
case $weight in
1) priostring=required ;;
2) priostring=recommended ;;
3) priostring=optional ;;
esac
case $qualifier in
a) buildstring=runtime ;;
b|f) buildstring= ;;
esac
dep_level=$DEP_LEVEL
if [ "$dep_level" = 3 ] && [ "$depth" -gt 2 ]; then dep_level=2; fi
if [ "$dep_level" -gt 3 ]; then dep_level=3; fi
echo -en "\nNode: $depth${spaceSTR:0:$(( depth + spacing ))}${RED}${depFile%.dep}${OFF} $priostring $buildstring"
depth=$(( depth + 1 ))
if (( depth < 10 )); then spacing=1; else spacing=0; fi
# Start of loop
{
while read prio_of_dep build_of_dep id_of_dep; do
case $prio_of_dep in
1) priostring=required ;;
2) priostring=recommended ;;
3) priostring=optional ;;
4) priostring=external ;;
esac
case $build_of_dep in
a ) buildstring=runtime ;;
b|f) buildstring= ;;
esac
# Has this entry already been seen?
# TODO: no there is no special case!
# We have a special case here: if the entry has been seen at depth > 2
# and now depth=2 and DEP_LEVEL=3, optional deps have not been processed.
# If this is the case, just consider it has not been seen.
if [ -f ${id_of_dep}.dep ] ; then
case $depth$DEP_LEVEL in
23) ;;
*)
# Just display it and proceed.
echo -en "\nEdge: $depth${spaceSTR:0:$((depth + spacing))}${MAGENTA}${id_of_dep}${OFF} $priostring $buildstring"
continue
;;
esac
fi
# Is the weight higher than requested?
if [ "$prio_of_dep" -gt $dep_level ]; then
# Just display it and proceed.
echo -en "\n Out: $depth${spaceSTR:0:$((depth + spacing))}${YELLOW}${id_of_dep}${OFF} $priostring $buildstring"
continue
fi
# Otherwise, let's build the corresponding subgraph.
xsltproc --stringparam idofdep "$id_of_dep" \
--stringparam MTA "$MAIL_SERVER" \
-o ${id_of_dep}.dep \
../xsl/dependencies.xsl ../packages.xml
if [[ -s ${id_of_dep}.dep ]]; then # this dependency has dependencies
generate_subgraph ${id_of_dep}.dep $prio_of_dep $depth $build_of_dep
else # id_of_dep has no dependencies, just touch the file and display
touch ${id_of_dep}.dep
echo -en "\nLeaf: $depth${spaceSTR:0:$((depth + spacing))}${CYAN}${id_of_dep}${OFF} $priostring $buildstring"
fi
done
} <$depFile
depth=$(( depth - 1 ))
if (( depth < 10 )); then spacing=1; else spacing=0; fi
echo -en "\n End: $depth${spaceSTR:0:$((depth + spacing))}${GREEN}${depFile%.dep}${OFF}"
return 0
}
#-----------#
path_to() { #
#-----------#
: <<inline_doc
function: check whether there is a path from $1 to $2 on the graph
input vars: $1 contains the filename of the starting node.
$2 contains the name of the node to reach
$3 contains the weight above which we do not want to
follow an edge
$seen (global) contains the list of already seen nodes.
It must ve set to " " prior to calling the function
returns: 0 if the node has been found
1 if not
on error: nothing
on success: nothing
inline_doc
local start=$1
local seek=$2
local prio=$3
local prio_of_dep
local build_of_dep
local id_of_dep
local r
if test "${start%.dep}" = "$seek"; then return 0; fi
seen="$seen${start%.dep} "
if test -s $start; then
{
while read prio_of_dep build_of_dep id_of_dep; do
if test "$prio" -lt "$prio_of_dep"; then continue; fi
if ! test "${seen% $id_of_dep *}" = "$seen"; then continue; fi
if path_to ${id_of_dep}.dep $seek $prio; then return 0; fi
done
} < $start
fi
return 1
}
#------------------#
clean_subgraph() { #
#------------------#
: <<inline_doc
function: Remove dangling edges and create groups of deps for "after"
deps: A-before->B-after->C becomes:
A -before-> Bgroupxx -before-> B
\
-before-> C
the name of the group is chosen so that it is unlikely as
a package name (so that it is removed when building the
xml book).
Also change the "first" qualifier so that a cycle:
A -first-> B ---chain---> A becomes:
B ---chain---> A -before-> B-pass1
and we remove all the dependencies which have a chain to
A in B-pass1.
Since we do not change anything else, it may happen that
nothing depends on B. In that case, B is appended to root.
input vars: None
files: <node>.dep files containing dangling edges and
"after" qualifiers
returns: 0
output: files: <node>.dep files containing no dangling edges and
no "after" qualifiers
on error: nothing
on success: nothing
inline_doc
local node
local id_of_dep
local prio_of_dep
local build_of_dep
local lines_to_remove
local lines_to_change
local parent
local p
local b
local start
local seen
for node in $(ls *.dep); do
if test $node = root.dep; then continue; fi
echo Cleaning $node
lines_to_remove=
{
while read prio_of_dep build_of_dep id_of_dep; do
if ! test -f ${id_of_dep}.dep; then
lines_to_remove="$lines_to_remove $id_of_dep"
continue
fi
done
} <$node
for id_of_dep in $lines_to_remove; do
sed "/\ $id_of_dep\$/d" -i $node
done
done
for node in $(grep -l ' a ' *.dep); do
lines_to_remove=
echo Process "runtime" deps in $node
if ! [ -e ${node%.dep}groupxx.dep ]; then
b=0 # Nothing depends on <node>groupxx
for parent in $(grep -l ${node%.dep}\$ *); do
p=0 # No "after" dependency depends on this parent
for start in $(grep ' a ' $node | cut -d' ' -f3); do
seen=" " # global variable used in "path_to"
if path_to ${start}.dep ${parent%.dep} 3; then p=1; break; fi
done
if test $p = 0; then
b=1
sed -i "s/\ ${node%.dep}\$/&groupxx/" $parent
fi
done
echo "1 b ${node%.dep}" > ${node%.dep}groupxx.dep
if test $b = 0; then echo "1 b ${node%.dep}groupxx" >> root.dep; fi
fi
{
while read prio_of_dep build_of_dep id_of_dep; do
if test $build_of_dep = a; then
if ! grep -q ${id_of_dep} ${node%.dep}groupxx.dep; then
echo "$prio_of_dep b ${id_of_dep}" >> ${node%.dep}groupxx.dep
fi
lines_to_remove="$lines_to_remove $id_of_dep"
fi
done
} <$node
for id_of_dep in $lines_to_remove; do
sed "/a\ $id_of_dep\$/d" -i $node
done
done
for node in $(grep -l ' f ' *); do
echo Process "first" deps in $node
lines_to_change=
{
while read prio_of_dep build_of_dep id_of_dep; do
if test $build_of_dep = f; then
if ! test -f ${id_of_dep}-pass1.dep; then
cp ${id_of_dep}{,-pass1}.dep;
fi
lines_to_change="$lines_to_change $id_of_dep"
unset lr # lines to remove in -pass1
{
while read p b start; do
seen=" " # global variable used in "path_to"
if path_to ${start}.dep ${node%.dep} $p; then
lr="$lr $start"
fi
done
} < ${id_of_dep}-pass1.dep
for p in $lr; do
sed "/\ $p\$/d" -i ${id_of_dep}-pass1.dep
done
fi
done
} <$node
for id_of_dep in $lines_to_change; do
sed "/\ $id_of_dep\$/"'{s/[[:digit:]] f /1 b /;s/$/-pass1/}' -i $node
if ! grep -q " $id_of_dep\$" *.dep; then
echo 1 b $id_of_dep >>root.dep
fi
done
done
} # End clean_subgraph
#----------------------------#
generate_dependency_tree() { #
#----------------------------#
: <<inline_doc
function: Create a subtree of the dependency tree
(recursive function)
input vars: $1 : file with a list of targets (child nodes)
the first line of the file is the link
$2 : priority (1=req, 2=rec, 3=opt)
returns: 0 if the tree has been successfully created
1 if we are backing up to the parent of a circular dep
and there are only required deps in the cycle
2 if we are backing up to the parent of a circular dep
and there are recommended deps and no optional deps in the
cycle
3 if we are backing up to the parent of a circular dep
and there are optiional deps in the cycle
modifies: vars: ParentNode is set when return is not 0
output: files: for each <pkg> with dependencies in $1,
a file <pkg>.tree and its dependencies
on error: nothing
on success: nothing
inline_doc
local depFile=$1
local priority=$2
local -a rootlink
local -a priolink
local -a otherlink
local -i depth
local -i count=0
local id_of_dep
local build_of_dep
local prio_of_dep
local parent
local lines_to_remove=
local srootlink
local priostring
local dpriostring
local i
{
read -a rootlink
depth=${#rootlink[*]}
read -a priolink
srootlink="${rootlink[*]} "
case $priority in
1) priostring=required ;;
2) priostring=recommended ;;
3) priostring=optional ;;
esac
# start of depFile
echo -en "\nNode: $depth${spaceSTR:0:$depth}${RED}${depFile%.tree}${OFF} $priostring"
while read prio_of_dep build_of_dep id_of_dep; do
case $prio_of_dep in
1) dpriostring=required ;;
2) dpriostring=recommended ;;
3) dpriostring=optional ;;
esac
# count entries in file
(( count++ ))
# Has this entry already been seen?
if [ -f ${id_of_dep}.tree ]; then # found ${id_of_dep}.tree already in tree
otherlink=($(head -n 1 ${id_of_dep}.tree))
if [ -z "${otherlink[*]}" ]
then echo otherlink empty for $id_of_dep.tree
echo This should not happen, but happens to happen...
exit 1
fi
#Do not use "${rootlink[*]}" =~ "${otherlink[*]}": case rootlink=(1 11)
# and otherlink=(1 1)
if [[ ${srootlink#"${otherlink[*]} "} != ${srootlink} ]]; then # cir. dep
echo -en "\nCirc: $((depth+1))${spaceSTR:0:$((depth+1))}${YELLOW}${id_of_dep}${OFF} $dpriostring"
# Find lowest priority in branch from parent to depFile:
p2=0
for (( i=${#otherlink[*]}; i < $depth ; i++ )) ; do
if (( ${priolink[i]} > $p2 )); then p2=${priolink[i]}; fi
done
if (( $prio_of_dep >= $p2 )); then # prune
lines_to_remove="$lines_to_remove $id_of_dep"
sed -i "/$id_of_dep/d" ${depFile/.tree/.dep}
else # find and set parent, then return lowest priority
# The parent has the same link without the last entry.
# We do not need otherlink anymore so just destroy the last element
unset otherlink[-1]
parentNode=$(grep ^"${otherlink[*]}"\$ -l *)
return $p2
fi
else # not circular: prune tree (but not .dep, since it may happen that
# the tree is destroyed and rebuilt in another order)
lines_to_remove="$lines_to_remove $id_of_dep"
fi # circular or not
continue # this dependency has already been seen, and the associated
# subtree computed. We are done
fi # Has this entry already been seen?
# So, this entry has not already been seen. Let's build the corresponding
# subtree. First check there is a subtree...
# Use -s, because it may happen that after removing lines, .dep exists
# but is empty.
if [[ -s ${id_of_dep}.dep ]]; then # this dependency has dependencies
sed "1i${rootlink[*]} $count\\
${priolink[*]} $prio_of_dep" < ${id_of_dep}.dep \
> ${id_of_dep}.tree # add link and priolink
generate_dependency_tree ${id_of_dep}.tree $prio_of_dep
# Test return value, in case we exchange dependencies
p2=$?
case $p2 in
0) # Normal return
;;
$prio_of_dep) # we remove this dep, but since it may become unreachable,
# move it to be built later (as a dep of parent).
tree_erase ${id_of_dep}.tree
lines_to_remove="$lines_to_remove $id_of_dep"
sed -i "/${id_of_dep}/d" ${depFile/.tree/.dep}
echo "$prio_of_dep b $id_of_dep" >> $parentNode
# must be added to .dep in case parentNode is destroyed when erasing
# the tree
echo "$prio_of_dep b $id_of_dep" >> ${parentNode/.tree/.dep}
continue
;;
*) # We are backing up
return $p2
;;
esac
else # id_of_dep has no dependencies, just record the link in a file
# and print
echo "${rootlink[*]} $count" > ${id_of_dep}.tree
echo -en "\nLeaf: $(($depth+1))${spaceSTR:0:$(($depth+1))}${CYAN}${id_of_dep}${OFF} $dpriostring"
fi
done
echo -en "\n End: $depth${spaceSTR:0:$depth}${GREEN}${depFile%.tree}${OFF}"
} <$depFile
# It may happen that a file is created with several times
# the same line. Normally, all those lines but one
# would be flagged to be removed (or all of them if
# the dependency appeared before). A simple sed /$line/d
# destroys all the lines. We should instead remove
# only one for each appearance of it in lines_to_remove.
# so first get the position of last line and then delete
# that line
for line in $lines_to_remove
do lineno=$(sed -n /^[[:digit:]]\ b\ $line\$/= $depFile | tail -n1)
sed -i ${lineno}d $depFile
done
return 0
}
#---------------#
tree_browse() { #
#---------------#
local file=$1
local f
#echo file=$file
for f in $(grep '[^0-9 ]' $file | sed 's/.* //'); do
# echo f=$f
if grep -q '[^0-9 ]' ${f}.tree ; then
tree_browse ${f}.tree
fi
echo $f
done
}
#--------------#
tree_erase() { #
#--------------#
local file=$1
local f
local rootlink
local rootlink2
#echo file=$file
rootlink="$(head -n1 $file) "
for f in $(grep '[^0-9 ]' $file | sed 's/.* //'); do
if [ -f ${f}.tree ]; then
rootlink2="$(head -n1 ${f}.tree) "
# We want two things:
# i) do not erase the file if it is in another branch
# ii) do not erase the file if there is a circular dependency
# for case i), we test that rootlink is contained in rootlink2
# for case ii), we test that rootlink2 is not contained in
# rootlink.
# See comment above about srootlink
if [[ ${rootlink2#${rootlink}} != ${rootlink2} &&
${rootlink#${rootlink2}} == ${rootlink} ]] ; then
tree_erase ${f}.tree
fi
fi
done
rm -f $file
}