• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CyberShadow / aconfmgr / 625

18 Nov 2025 11:56AM UTC coverage: 92.653% (+13.0%) from 79.69%
625

push

github

CyberShadow
.github/workflows/test.yml: Disable fail-fast

The integration test depends on flaky network services. We would like
to be able to retry just the failed jobs, which is not possible when
GitHub cancels all jobs when any fails.

3884 of 4192 relevant lines covered (92.65%)

390.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.89
/src/common.bash
1
# common.bash
2

3
# This file contains aconfmgr's common code, used by all commands.
4

5
####################################################################################################
6

7
# Globals
8

9
PACMAN=${PACMAN:-pacman}
90✔
10

11
output_dir="$tmp_dir"/output
90✔
12
system_dir="$tmp_dir"/system # Current system configuration, to be compared against the output directory
90✔
13

14
# The directory used for building AUR packages.
15
# When running as root, our "$tmp_dir" will be inaccessible to
16
# the user under which the building is performed ("nobody"),
17
# so we need a separate directory under this circumstance.
18
if [[ $EUID == 0 ]]
90✔
19
then
20
        aur_dir="$tmp_dir"-aur
×
21
else
22
        aur_dir="$tmp_dir"/aur
90✔
23
fi
24

25
default_file_mode=644
90✔
26

27
ANSI_clear_line=" [0K"
90✔
28
ANSI_color_R=" [1;31m"
90✔
29
ANSI_color_G=" [1;32m"
90✔
30
ANSI_color_Y=" [1;33m"
90✔
31
ANSI_color_B=" [1;34m"
90✔
32
ANSI_color_M=" [1;35m"
90✔
33
ANSI_color_C=" [1;36m"
90✔
34
ANSI_color_W=" [1;39m"
90✔
35
ANSI_reset=" [0m"
90✔
36

37
verbose=0
90✔
38
lint_config=false
90✔
39
umask $((666 - default_file_mode))
90✔
40

41
aconfmgr_action=
90✔
42
aconfmgr_action_args=()
90✔
43

44
####################################################################################################
45

46
# Defaults
47

48
# Initial ignore path list.
49
# Can be appended to using the IgnorePath helper.
50
ignore_paths=(
90✔
51
    '/dev'
90✔
52
    '/home'
90✔
53
    '/media'
90✔
54
    '/mnt'
90✔
55
    '/proc'
90✔
56
    '/root'
90✔
57
    '/run'
90✔
58
    '/sys'
90✔
59
    '/tmp'
90✔
60
    # '/var/.updated'
90✔
61
    '/var/cache'
90✔
62
    # '/var/lib'
90✔
63
    # '/var/lock'
90✔
64
    # '/var/log'
90✔
65
    # '/var/spool'
90✔
66
)
90✔
67

68
# These files must be installed before anything else,
69
# because they affect or are required for what follows.
70
# shellcheck disable=SC2034
71
priority_files=(
90✔
72
        /etc/passwd
90✔
73
        /etc/group
90✔
74
        /etc/pacman.conf
90✔
75
        /etc/pacman.d/mirrorlist
90✔
76
        /etc/makepkg.conf
90✔
77
)
90✔
78

79
# File content filters
80
# These are useful for files which contain some unpredictable element,
81
# such as a timestamp, which can't be considered as part of the system
82
# configuration, and can be safely omitted from the file.
83
# This is an associative array of patterns mapping to function
84
# names. The function is called with the file name as the only
85
# parameter, the file contents on its stdin, and is expected to
86
# provide the filtered contents on its stdout.
87
declare -A file_content_filters
90✔
88

89
# Some limits for common-sense warnings.
90
# Feel free to override these in your configuration.
91
warn_size_threshold=$((10*1024*1024)) # Warn on copying files bigger than this
90✔
92
warn_file_count_threshold=1000        # Warn on finding this many stray files
90✔
93
warn_tmp_df_threshold=$((1024*1024))  # Warn on error if free space in $tmp_dir is below this
90✔
94

95
makepkg_user=nobody # when running as root
90✔
96

97
####################################################################################################
98

99
function LogLeaveDirStats() {
100
        local dir="$1"
176✔
101
        Log 'Finalizing...\r'
176✔
102
        LogLeave 'Done (%s native packages, %s foreign packages, %s files).\n'        \
1,422✔
103
                         "$(Color G "$(wc -l < "$dir"/packages.txt)")"                                        \
×
104
                         "$(Color G "$(wc -l < "$dir"/foreign-packages.txt)")"                        \
×
105
                         "$(Color G "$(find "$dir"/files -not -type d | wc -l)")"
×
106
}
107

108
skip_config=n
90✔
109

110
# Run user configuration scripts, to collect desired state into #output_dir
111
function AconfCompileOutput() {
112
        LogEnter 'Compiling user configuration...\n'
92✔
113

114
        if [[ $skip_config == y ]]
92✔
115
        then
116
                LogLeave 'Skipped.\n'
1✔
117
                return
1✔
118
        fi
119

120
        # shellcheck disable=SC2174
121
        mkdir --mode=700 --parents "$tmp_dir"
91✔
122

123
        rm -rf "$output_dir"
91✔
124
        mkdir --parents "$output_dir"
91✔
125
        mkdir "$output_dir"/files
91✔
126
        touch "$output_dir"/packages.txt
91✔
127
        touch "$output_dir"/foreign-packages.txt
91✔
128
        touch "$output_dir"/file-props.txt
91✔
129
        touch "$output_dir"/warnings
91✔
130
        # shellcheck disable=SC2174
131
        mkdir --mode=700 --parents "$config_dir"
91✔
132

133
        # Configuration
134

135
        Log 'Using configuration in %s\n' "$(Color C "%q" "$config_dir")"
182✔
136

137
        typeset -ag ignore_packages=()
182✔
138
        typeset -ag ignore_foreign_packages=()
182✔
139
        typeset -Ag used_files
91✔
140

141
        local found=n
91✔
142
        local file
91✔
143
        for file in "$config_dir"/*.sh
166✔
144
        do
145
                if [[ -e "$file" ]]
166✔
146
                then
147
                        LogEnter 'Sourcing %s...\n' "$(Color C "%q" "$file")"
333✔
148
                        # shellcheck source=/dev/null
149
                        source "$file"
167✔
150
                        found=y
164✔
151
                        LogLeave ''
164✔
152
                fi
153
        done
154

155
        if $lint_config
89✔
156
        then
157
                # Check for unused files (files not referenced by the CopyFile
158
                # helper).
159
                # Only do this in the "check" action, as unused files do not
160
                # necessarily indicate a bug in the configuration - they may
161
                # simply be used under certain conditions.
162
                if [[ -d "$config_dir"/files ]]
3✔
163
                then
164
                        local line
2✔
165
                        find "$config_dir"/files -type f -print0 | \
2✔
166
                                while read -r -d $'\0' line
4✔
167
                                do
168
                                        local key=${line#"$config_dir"/files}
2✔
169
                                        if [[ -z "${used_files[$key]+x}" ]]
2✔
170
                                        then
171
                                                ConfigWarning 'Unused file: %s\n' \
2✔
172
                                                                          "$(Color C "%q" "$line")"
2✔
173
                                        fi
174
                                done
175
                fi
176
        fi
177

178
        if [[ $found == y ]]
89✔
179
        then
180
                LogLeaveDirStats "$output_dir"
89✔
181
        else
182
                LogLeave 'Done (configuration not found).\n'
×
183
        fi
184
}
185

186
skip_inspection=n
90✔
187
skip_checksums=n
90✔
188

189
# Given a list of paths to ignore (which can contain shell patterns),
190
# creates the list of arguments to give to 'find' to ignore them.
191
# The result is stored in the variable given by name as the first argument.
192
function AconfCreateFindIgnoreArgs() {
193
        # A correct and simple implementation of this function would just give
194
        # each argument as a parameter to find's -wholename, e.g. result in
195
        # (-wholename path1 -o -wholename path2 -o ... -o -wholename pathn)
196
        # However, this is painfully slow if the number of ignore patterns is large
197
        # Instead, this function (ab)uses the fact that if the number of patterns
198
        # given to GNU find is big, then as an implementation detail, using regular
199
        # expressions (-regex option and similar) scales much better than using
200
        # shell patterns (-wholename option and similar)
201
        local ignore_args_varname=$1
87✔
202
        local -n ignore_args_var=$ignore_args_varname
87✔
203
        shift
87✔
204
        local ignore_paths=("$@")
174✔
205

206
        # Divide the ignore paths as simple (contain obviously literal ASCII
207
        # characters or an asterisk wildcard) or complex (such as those using
208
        # character ranges, question mark wildcards, international characters, etc.)
209
        # Simple ignore paths are later going to be converted to regex,
210
        # complex ignore paths are passed literally to find's -wholename
211
        local simple_ignore_paths=()
174✔
212

213
        local ignore_path
87✔
214
        for ignore_path in "${ignore_paths[@]}"
1,970✔
215
        do
216
                # In this regular expression, we don't use ranges like "a-z", since this
217
                # can match non-ASCII characters. Also, the hyphen is at the end of the
218
                # regular expression to avoid it being interpreted as a range
219
                if [[ "$ignore_path" =~ [^abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_/\ .*-] ]]
1,970✔
220
                then
221
                        ignore_args_var+=(-wholename "$ignore_path" -o)
5✔
222
                else
223
                        simple_ignore_paths+=("${ignore_path}")
1,965✔
224
                fi
225
        done
226

227
        if [ ${#simple_ignore_paths[@]} -ne 0 ]
87✔
228
        then
229
                # Converting the simple ignore paths to regular expressions just needs to
230
                # handle the asterisk wildcard, and escape a few characters
231
                local ignore_regexps
87✔
232
                echo -n "${simple_ignore_paths[*]}" | \
87✔
233
                        sed 's|[^*abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_/ ]|[&]|g; s|\*|.*|g' | \
87✔
234
                        mapfile -t ignore_regexps
87✔
235

236
                ignore_args_var+=(-regex "$( IFS='|' ; echo "${ignore_regexps[*]}" )" -o)
261✔
237
        fi
238

239
        ignore_args_var+=(-false)
87✔
240
}
241

242
# Collect system state into $system_dir
243
function AconfCompileSystem() {
244
        LogEnter 'Inspecting system state...\n'
88✔
245

246
        if [[ $skip_inspection == y ]]
88✔
247
        then
248
                LogLeave 'Skipped.\n'
1✔
249
                return
1✔
250
        fi
251

252
        # shellcheck disable=SC2174
253
        mkdir --mode=700 --parents "$tmp_dir"
87✔
254

255
        rm -rf "$system_dir"
87✔
256
        mkdir --parents "$system_dir"
87✔
257
        mkdir "$system_dir"/files
87✔
258
        touch "$system_dir"/file-props.txt
87✔
259
        touch "$system_dir"/orig-file-props.txt
87✔
260

261
        ### Packages
262

263
        LogEnter 'Querying package list...\n'
87✔
264
        ( "$PACMAN" --query --quiet --explicit --native  || true ) | sort | ( grep -vFxf <(PrintArray ignore_packages        ) || true ) > "$system_dir"/packages.txt
350✔
265
        ( "$PACMAN" --query --quiet --explicit --foreign || true ) | sort | ( grep -vFxf <(PrintArray ignore_foreign_packages) || true ) > "$system_dir"/foreign-packages.txt
499✔
266
        LogLeave
87✔
267

268
        ### Files
269

270
        local -a found_files
87✔
271
        found_files=()
87✔
272

273
        # whether the contents is different from the original
274
        local -A found_file_edited
87✔
275
        found_file_edited=()
87✔
276

277
        # Stray files
278

279
        local -a ignore_args
87✔
280
        AconfCreateFindIgnoreArgs ignore_args "${ignore_paths[@]}"
87✔
281

282
        LogEnter 'Enumerating owned files...\n'
87✔
283
        mkdir --parents "$tmp_dir"
87✔
284
        ( "$PACMAN" --query --list --quiet || true ) | sed 's#\/$##' | sort --unique > "$tmp_dir"/owned-files
261✔
285
        LogLeave
87✔
286

287
        LogEnter 'Searching for stray files...\n'
87✔
288

289
        local line
87✔
290
        local -Ag ignored_dirs
87✔
291

292
        AconfNeedProgram gawk gawk n
87✔
293

294
        # Progress display - only show file names once per second
295
        local progress_fd
87✔
296
        exec {progress_fd}> \
174✔
297
                 >( gawk '
174✔
298
BEGIN {
×
299
    RS = "\0";
×
300
    t = systime();
×
301
};
×
302
{
303
    u = systime();
×
304
    if (t != u) {
×
305
        t = u;
×
306
        printf "%s\0", $0;
×
307
        system(""); # https://unix.stackexchange.com/a/83853/4830
×
308
        }
87✔
309
}' | \
×
310
                                while read -r -d $'\0' line
×
311
                                do
312
                                        local path=${line:1}
×
313
                                        path=${path%/*} # Never show files, only directories
×
314
                                        while [[ ${#path} -gt 40 ]]
×
315
                                        do
316
                                                path=${path%/*}
×
317
                                        done
318
                                        Log 'Scanning %s...\r' "$(Color M "%q" "$path")"
×
319
                                done
320
                  )
×
321

322
        local stray_file_count=0
87✔
323
        (
324
                # NB: Regular expressions can be generated by AconfCreateFindIgnoreArgs
325
                #     The posix-extended regex type is used since it's easier to work
326
                #     with (e.g. no need to escape '|' alternations, parenthesis, etc.)
327
                sudo find /                                                                        \
87✔
328
                         -regextype posix-extended                                \
×
329
                         -not                                                                        \
×
330
                         \(                                                                                \
×
331
                                 \(                                                                        \
×
332
                                        "${ignore_args[@]}"                                \
×
333
                                \)                                                                        \
×
334
                                -printf 'I' -print0 -prune                        \
×
335
                        \)                                                                                \
×
336
                        -printf 'O' -print0                                                \
×
337
                        | tee /dev/fd/$progress_fd                                \
87✔
338
                        | ( grep                                                                \
174✔
339
                                        --null --null-data                                \
×
340
                                        --invert-match                                        \
×
341
                                        --fixed-strings                                        \
×
342
                                        --line-regexp                                        \
×
343
                                        --file                                                        \
×
344
                                        <( < "$tmp_dir"/owned-files                \
×
345
                                                 sed -e 's#^#O#'                        \
×
346
                                         )                                                                \
×
347
                                        || true )                                                \
×
348
        ) |                                                                                                \
×
349
                while read -r -d $'\0' line
3,928✔
350
                do
351
                        local file action
3,977✔
352
                        file=${line:1}
4,009✔
353
                        action=${line:0:1}
4,014✔
354

355
                        case "$action" in
3,917✔
356
                                O) # Stray file
357
                                        #echo "ignore_paths+='$file' # "
358
                                        if ((verbose))
132✔
359
                                        then
360
                                                Log '%s\r' "$(Color C "%q" "$file")"
×
361
                                        fi
362

363
                                        found_files+=("$file")
136✔
364
                                        found_file_edited[$file]=y
131✔
365
                                        stray_file_count=$((stray_file_count+1))
130✔
366

367
                                        if [[ $stray_file_count -eq $warn_file_count_threshold ]]
129✔
368
                                        then
369
                                                LogEnter '%s: reached %s stray files while in directory %s.\n' \
×
370
                                                        "$(Color Y "Warning")" \
×
371
                                                        "$(Color G "$stray_file_count")" \
×
372
                                                        "$(Color C "%q" "$(dirname "$file")")"
×
373
                                                LogLeave 'Perhaps add %s (or a parent directory) to configuration to ignore it.\n' \
×
374
                                                                 "$(Color Y "IgnorePath %q" "$(dirname "$file")"/'*')"
×
375
                                                warn_file_count_threshold=$((warn_file_count_threshold * 10))
×
376
                                        fi
377
                                        ;;
378
                                I) # Ignored
379

380
                                        # For convenience, we want to also ignore
381
                                        # directories which contain only ignored files.
382
                                        #
383
                                        # This is so that a rule such as:
384
                                        #
385
                                        # IgnorePath '/foo/bar/baz/*.log'
386
                                        #
387
                                        # does not cause `aconfmgr save` to still emit lines like
388
                                        #
389
                                        # CreateDir /foo/bar
390
                                        # CreateDir /foo/bar/baz
391
                                        #
392
                                        # However, we can't simply exclude parent dirs of
393
                                        # excluded files from the file list, as then they
394
                                        # will show up as missing in the diff against the
395
                                        # compiled configuration. So, later we remove
396
                                        # parent directories of any found un-ignored
397
                                        # files.
398

399
                                        local path="$file"
3,785✔
400
                                        while [[ -n "$path" ]]
10,590✔
401
                                        do
402
                                                ignored_dirs[$path]=y
6,762✔
403
                                                path=${path%/*}
6,739✔
404
                                        done
405
                                        ;;
406
                        esac
407
                done
408

409
        LogLeave 'Done (%s stray files).\n' "$(Color G %s $stray_file_count)"
93✔
410

411
        exec {progress_fd}<&-
61✔
412

413
        LogEnter 'Cleaning up ignored files'\'' directories...\n'
61✔
414

415
        local file
60✔
416
        for file in "${found_files[@]}"
146✔
417
        do
418
                if [[ -z "${ignored_dirs[$file]+x}" ]]
115✔
419
                then
420
                        local path="$file"
44✔
421
                        while [[ -n "$path" ]]
89✔
422
                        do
423
                                unset "ignored_dirs[\$path]"
47✔
424
                                path=${path%/*}
47✔
425
                        done
426
                fi
427
        done
428

429
        LogLeave
24✔
430

431
        # Modified files
432

433
        LogEnter 'Searching for modified files...\n'
25✔
434

435
        AconfNeedProgram paccheck pacutils n
27✔
436
        AconfNeedProgram unbuffer expect n
30✔
437
        local modified_file_count=0
29✔
438
        local -A saw_file
26✔
439

440
        # Tentative tracking of original file properties.
441
        # The canonical version is read from orig-file-props.txt in AconfAnalyzeFiles
442
        unset orig_file_props ; typeset -Ag orig_file_props
53✔
443

444
        : > "$tmp_dir"/file-owners
28✔
445

446
        local paccheck_opts=(unbuffer paccheck --files --file-properties --backup --noupgrade)
75✔
447
        if [[ $skip_checksums == n ]]
41✔
448
        then
449
                # Use SHA256 for pacman 7.0+ (libalpm v15+) which uses SHA256 in mtree
450
                # Fall back to MD5 for older versions
451
                if paccheck --help 2>&1 | grep -q -- '--sha256sum'
91✔
452
                then
453
                        paccheck_opts+=(--sha256sum)
84✔
454
                else
455
                        paccheck_opts+=(--md5sum)
456
                fi
457
        fi
458

459
        sudo sh -c "LC_ALL=C stdbuf -o0 $(printf ' %q' "${paccheck_opts[@]}") 2>&1 || true" | \
172✔
460
                while read -r line
62,832✔
461
                do
462
                        if [[ $line =~ ^(.*):\ \'(.*)\'\ (type|size|modification\ time|md5sum|sha256sum|UID|GID|permission|symlink\ target)\ mismatch\ \(expected\ (.*)\)$ ]]
62,477✔
463
                        then
464
                                local package="${BASH_REMATCH[1]}"
2,031✔
465
                                local file="${BASH_REMATCH[2]}"
2,034✔
466
                                local kind="${BASH_REMATCH[3]}"
2,027✔
467
                                local value="${BASH_REMATCH[4]}"
2,023✔
468

469
                                local ignored=n
2,018✔
470
                                local ignore_path
2,017✔
471
                                for ignore_path in "${ignore_paths[@]}"
36,003✔
472
                                do
473
                                        # shellcheck disable=SC2053
474
                                        if [[ "$file" == $ignore_path ]]
35,884✔
475
                                        then
476
                                                ignored=y
1,590✔
477
                                                break
1,581✔
478
                                        fi
479
                                done
480

481
                                if [[ $ignored == n ]]
1,598✔
482
                                then
483
                                        if [[ -z "${saw_file[$file]+x}" ]]
25✔
484
                                        then
485
                                                saw_file[$file]=y
13✔
486
                                                Log '%s: %s\n' "$(Color M "%q" "$package")" "$(Color C "%q" "$file")"
77✔
487
                                                found_files+=("$file")
32✔
488
                                                modified_file_count=$((modified_file_count+1))
32✔
489
                                        fi
490

491
                                        local prop
45✔
492
                                        case "$kind" in
47✔
493
                                                UID)
494
                                                        prop=owner
8✔
495
                                                        value=${value#*/}
7✔
496
                                                        ;;
497
                                                GID)
498
                                                        prop=group
6✔
499
                                                        value=${value#*/}
6✔
500
                                                        ;;
501
                                                permission)
502
                                                        prop=mode
6✔
503
                                                        ;;
504
                                                type|size|modification\ time|md5sum|sha256sum|symlink\ target)
505
                                                        prop=
27✔
506
                                                        found_file_edited[$file]=y
27✔
507
                                                        ;;
508
                                                *)
509
                                                        prop=
×
510
                                                        ;;
511
                                        esac
512

513
                                        if [[ -n "$prop" ]]
46✔
514
                                        then
515
                                                local key="$file:$prop"
19✔
516
                                                orig_file_props[$key]=$value
18✔
517

518
                                                printf '%s\t%s\t%q\n' "$prop" "$value" "$file" >> "$system_dir"/orig-file-props.txt
20✔
519
                                        fi
520
                                fi
521
                                printf '%s\0%s\0' "$file" "$package" >> "$tmp_dir"/file-owners
1,627✔
522
                        elif [[ $line =~ ^(.*):\ \'(.*)\'\ missing\ file$ ]]
60,970✔
523
                        then
524
                                local package="${BASH_REMATCH[1]}"
13✔
525
                                local file="${BASH_REMATCH[2]}"
13✔
526

527
                                local ignored=n
13✔
528
                                local ignore_path
13✔
529
                                for ignore_path in "${ignore_paths[@]}"
236✔
530
                                do
531
                                        # shellcheck disable=SC2053
532
                                        if [[ "$file" == $ignore_path ]]
232✔
533
                                        then
534
                                                ignored=y
2✔
535
                                                break
2✔
536
                                        fi
537
                                done
538

539
                                if [[ $ignored == y ]]
7✔
540
                                then
541
                                        continue
2✔
542
                                fi
543

544
                                Log '%s (missing)...\r' "$(Color M "%q" "$package")"
18✔
545
                                printf '%s\t%s\t%q\n' "deleted" "y" "$file" >> "$system_dir"/file-props.txt
9✔
546
                                printf '%s\0%s\0' "$file" "$package" >> "$tmp_dir"/file-owners
9✔
547
                        elif [[ $line =~ ^warning:\ (.*):\ \'(.*)\'\ read\ error\ \(No\ such\ file\ or\ directory\)$ ]]
60,728✔
548
                        then
549
                                local package="${BASH_REMATCH[1]}"
10✔
550
                                local file="${BASH_REMATCH[2]}"
9✔
551
                                # Ignore
552
                        elif [[ $line =~ ^(.*):\ all\ files\ match\ (database|mtree|mtree\ md5sums|mtree\ sha256sums)$ ]]
60,715✔
553
                        then
554
                                local package="${BASH_REMATCH[1]}"
61,131✔
555
                                Log '%s...\r' "$(Color M "%q" "$package")"
123,391✔
556
                                #echo "Now at ${BASH_REMATCH[1]}"
557
                        else
558
                                Log 'Unknown paccheck output line: %s\n' "$(Color Y "%q" "$line")"
9✔
559
                        fi
560
                done
561
        LogLeave 'Done (%s modified files).\n' "$(Color G %s $modified_file_count)"
174✔
562

563
        LogEnter 'Reading file attributes...\n'
87✔
564

565
        typeset -a found_file_types found_file_sizes found_file_modes found_file_owners found_file_groups
87✔
566
        if [[ ${#found_files[*]} == 0 ]]
87✔
567
        then
568
                Log 'No files found, skipping.\n'
×
569
        else
570
                Log 'Reading file types...\n'  ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%F | mapfile -t  found_file_types
348✔
571
                Log 'Reading file sizes...\n'  ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%s | mapfile -t  found_file_sizes
348✔
572
                Log 'Reading file modes...\n'  ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%a | mapfile -t  found_file_modes
348✔
573
                Log 'Reading file owners...\n' ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%U | mapfile -t found_file_owners
348✔
574
                Log 'Reading file groups...\n' ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%G | mapfile -t found_file_groups
348✔
575
        fi
576

577
        LogLeave # Reading file attributes
87✔
578

579
        LogEnter 'Checking disk space...\n'
87✔
580
        local -i i
87✔
581
        local -i total_blocks=0
87✔
582
        local -i tmp_block_size tmp_blocks_free
87✔
583
        tmp_block_size=$(stat -f -c %S "$system_dir")
174✔
584
        tmp_blocks_free=$(stat -f -c %f "$system_dir")
174✔
585
        local tmp_fs_type
87✔
586
        tmp_fs_type=$(stat -f -c %T "$system_dir")
174✔
587
        if [[ "$tmp_fs_type" != "ramfs" ]]
87✔
588
        then
589
                for ((i=0; i<${#found_files[*]}; i++))
1,012✔
590
                do
591
                        local -i size="${found_file_sizes[$i]}"
419✔
592
                        local -i blocks=$(((size+tmp_block_size-1)/tmp_block_size)) # Count blocks
419✔
593
                        total_blocks+=$blocks
419✔
594
                        if (( total_blocks >= tmp_blocks_free ))
419✔
595
                        then
596
                                local file="${found_files[$i]}"
×
597
                                Log 'Copying file %s (%s bytes / %s blocks) to temporary storage would exhaust free space on %s (%s bytes / %s blocks).\n' \
×
598
                                        "$(Color C "%q" "$file")" "$(Color G "$size")" "$(Color G "$blocks")" \
×
599
                                        "$(Color C "%q" "$system_dir")" "$(Color G "$((tmp_blocks_free * tmp_block_size))")" "$(Color G "$tmp_blocks_free")"
×
600
                                Log 'Perhaps add %s (or a parent directory) to configuration to ignore it, or run with %s pointing at another location.\n' \
×
601
                                        "$(Color Y "IgnorePath %q" "$(dirname "$file")"/'*')" "$(Color Y "TMPDIR")"
×
602
                                FatalError 'Refusing to proceed.\n'
×
603
                        fi
604
                done
605
        fi
606
        LogLeave
87✔
607

608
        LogEnter 'Processing found files...\n'
87✔
609

610
        for ((i=0; i<${#found_files[*]}; i++))
1,012✔
611
        do
612
                Log '%s/%s...\r' "$(Color G "$i")" "$(Color G "${#found_files[*]}")"
1,257✔
613

614
                local  file="${found_files[$i]}"
419✔
615
                local  type="${found_file_types[$i]}"
419✔
616
                local  size="${found_file_sizes[$i]}"
419✔
617
                local  mode="${found_file_modes[$i]}"
419✔
618
                local owner="${found_file_owners[$i]}"
419✔
619
                local group="${found_file_groups[$i]}"
419✔
620

621
                if [[ "${ignored_dirs[$file]-n}" == y ]]
419✔
622
                then
623
                        continue
261✔
624
                fi
625

626
                if [[ -n "${found_file_edited[$file]+x}" ]]
158✔
627
                then
628
                        mkdir --parents "$(dirname "$system_dir"/files/"$file")"
306✔
629
                        if [[ "$type" == "symbolic link" ]]
153✔
630
                        then
631
                                ln -s -- "$(sudo readlink "$file")" "$system_dir"/files/"$file"
36✔
632
                        elif [[ "$type" == "regular file" || "$type" == "regular empty file" ]]
238✔
633
                        then
634
                                if [[ $size -gt $warn_size_threshold ]]
34✔
635
                                then
636
                                        Log '%s: copying large file %s (%s bytes). Add %s to configuration to ignore.\n' "$(Color Y "Warning")" "$(Color C "%q" "$file")" "$(Color G "$size")" "$(Color Y "IgnorePath %q" "$file")"
×
637
                                fi
638

639
                                local filter_pattern filter_func
34✔
640
                                unset filter_func
34✔
641
                                for filter_pattern in "${!file_content_filters[@]}"
3✔
642
                                do
643
                                        # shellcheck disable=SC2053
644
                                        if [[ "$file" == $filter_pattern ]]
3✔
645
                                        then
646
                                                filter_func=${file_content_filters[$filter_pattern]}
3✔
647
                                        fi
648
                                done
649

650
                                if [[ -v filter_func ]]
34✔
651
                                then
652
                                        sudo cat "$file" | "$filter_func" "$file" > "$system_dir"/files/"$file"
6✔
653
                                else
654
                                        # shellcheck disable=SC2024
655
                                        sudo cat "$file" > "$system_dir"/files/"$file"
31✔
656
                                fi
657
                        elif [[ "$type" == "directory" ]]
101✔
658
                        then
659
                                mkdir --parents "$system_dir"/files/"$file"
101✔
660
                        else
661
                                Log '%s: Skipping file %s with unknown type %s. Add %s to configuration to ignore.\n' "$(Color Y "Warning")" "$(Color C "%q" "$file")" "$(Color G "$type")" "$(Color Y "IgnorePath %q" "$file")"
×
662
                                continue
×
663
                        fi
664
                fi
665

666
                {
667
                        local prop
158✔
668
                        for prop in mode owner group
474✔
669
                        do
670
                                # Ignore mode "changes" in symbolic links
671
                                # If a file's type changes, a change in mode can be reported too.
672
                                # But, symbolic links cannot have a mode, so ignore this change.
673
                                if [[ "$type" == "symbolic link" && "$prop" == mode ]]
528✔
674
                                then
675
                                        continue
18✔
676
                                fi
677

678
                                local value
456✔
679
                                eval "value=\$$prop"
912✔
680

681
                                local default_value
456✔
682

683
                                if [[ $i -lt $stray_file_count ]]
456✔
684
                                then
685
                                        # For stray files, the default owner/group is root/root,
686
                                        # and the default mode depends on the type.
687
                                        # Let AconfDefaultFileProp get the correct default value for us.
688

689
                                        default_value=
372✔
690
                                else
691
                                        # For owned files, we assume that the defaults are the
692
                                        # files' current properties, unless paccheck said
693
                                        # otherwise.
694

695
                                        default_value=$value
84✔
696
                                fi
697

698
                                local orig_value
456✔
699
                                orig_value=$(AconfDefaultFileProp "$file" "$prop" "$type" "$default_value")
912✔
700

701
                                [[ "$value" == "$orig_value" ]] || printf '%s\t%s\t%q\n' "$prop" "$value" "$file"
519✔
702
                        done
703
                } >> "$system_dir"/file-props.txt
×
704
        done
705

706
        LogLeave # Processing found files
87✔
707

708
        LogLeaveDirStats "$system_dir" # Inspecting system state
87✔
709
}
710

711
####################################################################################################
712

713
typeset -A file_property_kind_exists
90✔
714

715
# Print to stdout the original/default value of the given file property.
716
# Uses orig_file_props entry if present.
717
function AconfDefaultFileProp() {
718
        local file=$1 # Absolute path to the file
471✔
719
        local prop=$2 # Name of the property (owner, group, or mode)
471✔
720
        local type="${3:-}" # Type of the file, as identified by `stat --format=%F`
471✔
721
        local default="${4:-}" # Default value, returned if we don't know the original file property.
471✔
722

723
        local key="$file:$prop"
471✔
724

725
        if [[ -n "${orig_file_props[$key]+x}" ]]
471✔
726
        then
727
                printf '%s' "${orig_file_props[$key]}"
50✔
728
                return
50✔
729
        fi
730

731
        if [[ -n "$default" ]]
421✔
732
        then
733
                printf '%s' "$default"
40✔
734
                return
40✔
735
        fi
736

737
        case "$prop" in
381✔
738
                mode)
739
                        if [[ -z "$type" ]]
125✔
740
                        then
741
                                type=$(sudo env LC_ALL=C stat --format=%F "$file")
6✔
742
                        fi
743

744
                        if [[ "$type" == "symbolic link" ]]
125✔
745
                        then
746
                                FatalError 'Symbolic links do not have a mode\n' # Bug
×
747
                        elif [[ "$type" == "directory" ]]
125✔
748
                        then
749
                                printf 755
97✔
750
                        else
751
                                printf '%s' "$default_file_mode"
28✔
752
                        fi
753
                        ;;
754
                owner|group)
755
                        printf 'root'
256✔
756
                        ;;
757
        esac
758
}
759

760
# Read a file-props.txt file into an associative array.
761
function AconfReadFileProps() {
762
        local filename="$1" # Path to file-props.txt to be read
255✔
763
        local varname="$2"  # Name of global associative array variable to read into
255✔
764

765
        local line
255✔
766
        while read -r line
741✔
767
        do
768
                if [[ $line =~ ^(.*)\        (.*)\        (.*)$ ]]
486✔
769
                then
770
                        local kind="${BASH_REMATCH[1]}"
486✔
771
                        local value="${BASH_REMATCH[2]}"
486✔
772
                        local file="${BASH_REMATCH[3]}"
486✔
773
                        file="$(eval "printf %s $file")" # Unescape
1,458✔
774

775
                        if [[ -z "$value" ]]
486✔
776
                        then
777
                                unset "${varname}[\$file:\$kind]"
119✔
778
                        else
779
                                eval "${varname}[\$file:\$kind]=\"\$value\""
734✔
780
                        fi
781

782
                        file_property_kind_exists[$kind]=y
486✔
783
                fi
784
        done < "$filename"
×
785
}
786

787
# Compare file properties.
788
function AconfCompareFileProps() {
789
        LogEnter 'Comparing file properties...\n'
166✔
790

791
        typeset -ag system_only_file_props=()
332✔
792
        typeset -ag changed_file_props=()
332✔
793
        typeset -ag config_only_file_props=()
332✔
794

795
        local key
166✔
796
        for key in "${!system_file_props[@]}"
143✔
797
        do
798
                if [[ -z "${output_file_props[$key]+x}" ]]
143✔
799
                then
800
                        system_only_file_props+=("$key")
39✔
801
                fi
802
        done
803

804
        for key in "${!system_file_props[@]}"
136✔
805
        do
806
                if [[ -n "${output_file_props[$key]+x}" && "${system_file_props[$key]}" != "${output_file_props[$key]}" ]]
232✔
807
                then
808
                        changed_file_props+=("$key")
29✔
809
                fi
810
        done
811

812
        for key in "${!output_file_props[@]}"
121✔
813
        do
814
                if [[ -z "${system_file_props[$key]+x}" ]]
121✔
815
                then
816
                        config_only_file_props+=("$key")
50✔
817
                fi
818
        done
819

820
        LogLeave
163✔
821
}
822

823
# Compare file information in $output_dir and $system_dir.
824
function AconfAnalyzeFiles() {
825

826
        #
827
        # Stray/modified files - diff
828
        #
829

830
        LogEnter 'Examining files...\n'
85✔
831

832
        LogEnter 'Loading data...\n'
85✔
833
        mkdir --parents "$tmp_dir"
85✔
834
        ( cd "$output_dir"/files && find . -mindepth 1 -print0 ) | cut --zero-terminated -c 2- | sort --zero-terminated > "$tmp_dir"/output-files
340✔
835
        ( cd "$system_dir"/files && find . -mindepth 1 -print0 ) | cut --zero-terminated -c 2- | sort --zero-terminated > "$tmp_dir"/system-files
340✔
836
        LogLeave
85✔
837

838
        Log 'Comparing file data...\n'
85✔
839

840
        typeset -ag system_only_files=()
170✔
841
        local file
85✔
842

843
        ( comm -13 --zero-terminated "$tmp_dir"/output-files "$tmp_dir"/system-files ) | \
85✔
844
                while read -r -d $'\0' file
119✔
845
                do
846
                        Log 'Only in system: %s\n' "$(Color C "%q" "$file")"
68✔
847
                        system_only_files+=("$file")
34✔
848
                done
849

850
        typeset -ag changed_files=()
170✔
851

852
        AconfNeedProgram diff diffutils n
85✔
853

854
        ( comm -12 --zero-terminated "$tmp_dir"/output-files "$tmp_dir"/system-files ) | \
85✔
855
                while read -r -d $'\0' file
136✔
856
                do
857
                        local output_type system_type
51✔
858
                        output_type=$(LC_ALL=C stat --format=%F "$output_dir"/files/"$file")
153✔
859
                        system_type=$(LC_ALL=C stat --format=%F "$system_dir"/files/"$file")
153✔
860

861
                        if [[ "$output_type" != "$system_type" ]]
51✔
862
                        then
863
                                Log 'Changed type (%s / %s): %s\n' \
80✔
864
                                        "$(Color Y "%q" "$output_type")" \
80✔
865
                                        "$(Color Y "%q" "$system_type")" \
80✔
866
                                        "$(Color C "%q" "$file")"
80✔
867
                                changed_files+=("$file")
20✔
868
                                continue
20✔
869
                        fi
870

871
                        if [[ "$output_type" == "directory" || "$system_type" == "directory" ]]
55✔
872
                        then
873
                                continue
7✔
874
                        fi
875

876
                        if ! diff --no-dereference --brief "$output_dir"/files/"$file" "$system_dir"/files/"$file" > /dev/null
24✔
877
                        then
878
                                Log 'Changed: %s\n' "$(Color C "%q" "$file")"
18✔
879
                                changed_files+=("$file")
9✔
880
                        fi
881
                done
882

883
        typeset -ag config_only_files=()
170✔
884

885
        ( comm -23 --zero-terminated "$tmp_dir"/output-files "$tmp_dir"/system-files ) | \
85✔
886
                while read -r -d $'\0' file
118✔
887
                do
888
                        Log 'Only in config: %s\n' "$(Color C "%q" "$file")"
66✔
889
                        config_only_files+=("$file")
33✔
890
                done
891

892
        LogLeave 'Done (%s only in system, %s changed, %s only in config).\n'        \
340✔
893
                         "$(Color G "${#system_only_files[@]}")"                                                \
340✔
894
                         "$(Color G "${#changed_files[@]}")"                                                        \
340✔
895
                         "$(Color G "${#config_only_files[@]}")"
340✔
896

897
        #
898
        # Modified file properties
899
        #
900

901
        LogEnter 'Examining file properties...\n'
85✔
902

903
        LogEnter 'Loading data...\n'
85✔
904
        unset orig_file_props # Also populated by AconfCompileSystem, so that it can be used by AconfDefaultFileProp
85✔
905
        typeset -Ag output_file_props ; AconfReadFileProps "$output_dir"/file-props.txt output_file_props
170✔
906
        typeset -Ag system_file_props ; AconfReadFileProps "$system_dir"/file-props.txt system_file_props
170✔
907
        typeset -Ag   orig_file_props ; AconfReadFileProps "$system_dir"/orig-file-props.txt orig_file_props
170✔
908
        LogLeave
85✔
909

910
        typeset -ag all_file_property_kinds
85✔
911
        all_file_property_kinds=("${!file_property_kind_exists[@]}")
85✔
912
        Print0Array all_file_property_kinds | sort --zero-terminated | mapfile -t -d $'\0' all_file_property_kinds
255✔
913

914
        AconfCompareFileProps
85✔
915

916
        LogLeave 'Done (%s only in system, %s changed, %s only in config).\n'        \
337✔
917
                         "$(Color G "${#system_only_file_props[@]}")"                                        \
337✔
918
                         "$(Color G "${#changed_file_props[@]}")"                                                \
337✔
919
                         "$(Color G "${#config_only_file_props[@]}")"
337✔
920
}
921

922
# The *_packages arrays are passed by name,
923
# so ShellCheck thinks the variables are unused:
924
# shellcheck disable=2034
925

926
# Prepare configuration and system state
927
function AconfCompile() {
928
        LogEnter 'Collecting data...\n'
84✔
929

930
        # Configuration
931

932
        AconfCompileOutput
84✔
933

934
        # System
935

936
        AconfCompileSystem
84✔
937

938
        # Vars
939

940
        < "$output_dir"/packages.txt         sort --unique | mapfile -t                   packages
168✔
941
        < "$system_dir"/packages.txt         sort --unique | mapfile -t         installed_packages
168✔
942

943
        < "$output_dir"/foreign-packages.txt sort --unique | mapfile -t           foreign_packages
168✔
944
        < "$system_dir"/foreign-packages.txt sort --unique | mapfile -t installed_foreign_packages
168✔
945

946
        AconfAnalyzeFiles
84✔
947

948
        LogLeave # Collecting data
84✔
949
}
950

951
####################################################################################################
952

953
pacman_opts=("$PACMAN")
90✔
954
aurman_opts=(aurman)
90✔
955
pacaur_opts=(pacaur)
90✔
956
yaourt_opts=(yaourt)
90✔
957
yay_opts=(yay)
90✔
958
paru_opts=(paru)
90✔
959
aura_opts=(aura)
90✔
960
makepkg_opts=(makepkg)
90✔
961
diff_opts=(diff '--color=auto')
90✔
962

963
aur_helper=
90✔
964
aur_helpers=(aurman pacaur yaourt yay paru aura makepkg)
90✔
965

966
# Only aconfmgr can use makepkg under root
967
if [[ $EUID == 0 ]]
90✔
968
then
969
        aur_helper=makepkg
×
970
fi
971

972
function DetectAurHelper() {
973
        if [[ -n "$aur_helper" ]]
40✔
974
        then
975
                return
32✔
976
        fi
977

978
        LogEnter 'Detecting AUR helper...\n'
8✔
979

980
        local helper
8✔
981
        for helper in "${aur_helpers[@]}"
56✔
982
        do
983
                if hash "$helper" 2> /dev/null
56✔
984
                then
985
                        aur_helper=$helper
8✔
986
                        LogLeave '%s... Yes\n' "$(Color C %s "$helper")"
16✔
987
                        return
8✔
988
                fi
989
                Log '%s... No\n' "$(Color C %s "$helper")"
96✔
990
        done
991

992
        Log 'Can'\''t find even makepkg!?\n'
×
993
        Exit 1
×
994
}
995

996
base_devel_installed=n
90✔
997

998
# Query AUR RPC API to find the package base for a given package name.
999
# This is used to resolve virtual provides and split packages without
1000
# requiring auracle-git to be installed (which would create circular dependencies).
1001
function AconfQueryAURPackageBase() {
1002
        local package=$1
1✔
1003
        local aur_rpc_url="https://aur.archlinux.org/rpc/?v=5"
1✔
1004
        local pkg_base=""
1✔
1005

1006
        # Try exact package name match first
1007
        local response resultcount
1✔
1008
        response=$(curl -fsSL "${aur_rpc_url}&type=info&arg=${package}" 2>/dev/null || true)
2✔
1009

1010
        if [[ -n "$response" ]]
1✔
1011
        then
1012
                resultcount=$(printf '%s' "$response" | sed -n 's/.*"resultcount":\([0-9]*\).*/\1/p')
3✔
1013
                if [[ "$resultcount" -gt 0 ]]
1✔
1014
                then
1015
                        pkg_base=$(printf '%s' "$response" | grep -o '"PackageBase":"[^"]*"' | head -1 | sed 's/"PackageBase":"\([^"]*\)"/\1/')
5✔
1016
                fi
1017
        fi
1018

1019
        # If not found, search by provides (for virtual packages like 'glaze' provided by 'glaze-git')
1020
        if [[ -z "$pkg_base" ]]
1✔
1021
        then
1022
                response=$(curl -fsSL "${aur_rpc_url}&type=search&by=provides&arg=${package}" 2>/dev/null || true)
1023

1024
                if [[ -n "$response" ]]
×
1025
                then
1026
                        resultcount=$(printf '%s' "$response" | sed -n 's/.*"resultcount":\([0-9]*\).*/\1/p')
×
1027
                        if [[ "$resultcount" -gt 0 ]]
×
1028
                        then
1029
                                pkg_base=$(printf '%s' "$response" | grep -o '"PackageBase":"[^"]*"' | head -1 | sed 's/"PackageBase":"\([^"]*\)"/\1/')
×
1030
                        fi
1031
                fi
1032
        fi
1033

1034
        printf '%s' "$pkg_base"
1✔
1035
}
1036

1037
function AconfMakePkg() {
1038
        local install=true
23✔
1039
        if [[ "$1" == --noinstall ]]
23✔
1040
        then
1041
                install=false
3✔
1042
                shift
3✔
1043
        fi
1044

1045
        local package="$1"
23✔
1046
        local asdeps="${2:-false}"
23✔
1047

1048
        LogEnter 'Building foreign package %s from source.\n' "$(Color M %q "$package")"
46✔
1049

1050
        # shellcheck disable=SC2174
1051
        mkdir --parents --mode=700 "$aur_dir"
23✔
1052
        if [[ $EUID == 0 ]]
23✔
1053
        then
1054
                chown -R "$makepkg_user": "$aur_dir"
×
1055
        fi
1056

1057
        local pkg_dir="$aur_dir"/"$package"
23✔
1058
        Log 'Using directory %s.\n' "$(Color C %q "$pkg_dir")"
46✔
1059

1060
        rm -rf "$pkg_dir"
23✔
1061
        mkdir --parents "$pkg_dir"
23✔
1062

1063
        # Needed to clone the AUR repo. Should be replaced with curl/tar.
1064
        AconfNeedProgram git git n
23✔
1065

1066
        if [[ $base_devel_installed == n ]]
23✔
1067
        then
1068
                LogEnter 'Making sure the %s package is installed...\n' "$(Color M base-devel)"
30✔
1069
                ParanoidConfirm ''
15✔
1070
                if ! "$PACMAN" --query --quiet base-devel > /dev/null 2>&1
15✔
1071
                then
1072
                        AconfInstallNative base-devel
1✔
1073
                fi
1074

1075
                LogLeave
15✔
1076
                base_devel_installed=y
15✔
1077
        fi
1078

1079
        LogEnter 'Cloning...\n'
23✔
1080
        git clone "https://aur.archlinux.org/$package.git" "$pkg_dir"
23✔
1081
        LogLeave
23✔
1082

1083
        if [[ ! -f "$pkg_dir"/PKGBUILD ]]
23✔
1084
        then
1085
                Log 'No package description file found!\n'
1✔
1086

1087
                if [[ "$package" == auracle-git ]]
1✔
1088
                then
1089
                        FatalError 'Failed to download aconfmgr dependency!\n'
×
1090
                fi
1091

1092
                LogEnter 'Assuming this package is part of a package base:\n'
1✔
1093

1094
                LogEnter 'Retrieving package info from AUR...\n'
1✔
1095
                local pkg_base
1✔
1096
                pkg_base=$(AconfQueryAURPackageBase "$package")
2✔
1097

1098
                if [[ -z "$pkg_base" ]]
1✔
1099
                then
1100
                        # Fallback to auracle if AUR RPC fails
1101
                        Log 'AUR RPC query failed, falling back to auracle...\n'
×
1102
                        AconfNeedProgram auracle auracle-git y
×
1103
                        pkg_base=$(auracle info --format '{pkgbase}' "$package")
1104
                fi
1105
                LogLeave 'Done, package base is %s.\n' "$(Color M %q "$pkg_base")"
2✔
1106

1107
                AconfMakePkg "$pkg_base" "$asdeps" # recurse
1✔
1108
                LogLeave # Package base
1✔
1109
                LogLeave # Package
1✔
1110
                return
1✔
1111
        fi
1112

1113
        AconfMakePkgDir "$package" "$asdeps" "$install" "$pkg_dir"
22✔
1114
}
1115

1116
function AconfMakePkgDir() {
1117
        local package=$1
22✔
1118
        local asdeps=$2
22✔
1119
        local install=$3
22✔
1120
        local pkg_dir=$4
22✔
1121

1122
        local gnupg_home
22✔
1123
        gnupg_home="$(realpath -m "$tmp_dir/gnupg")"
44✔
1124

1125
        local infofile infofilename
22✔
1126
        for infofilename in .SRCINFO .AURINFO
44✔
1127
        do
1128
                infofile="$pkg_dir"/"$infofilename"
44✔
1129
                if test -f "$infofile"
44✔
1130
                then
1131
                        LogEnter 'Checking dependencies...\n'
22✔
1132

1133
                        local depends missing_depends dependency arch
22✔
1134
                        arch="$(uname -m)"
44✔
1135
                        # Filter out packages from the same base
1136
                        ( grep -E $'^\t(make|check)?depends(_'"$arch"')? = ' "$infofile" || true ) \
32✔
1137
                                | sed 's/^.* = \([^<>=]*\)\([<>=].*\)\?$/\1/g' \
22✔
1138
                                | ( grep -vFf <(( grep '^pkgname = ' "$infofile" || true) \
66✔
1139
                                                                        | sed 's/^.* = \(.*\)$/\1/g' ) \
×
1140
                                                || true ) \
10✔
1141
                                | mapfile -t depends
22✔
1142

1143
                        if [[ ${#depends[@]} != 0 ]]
22✔
1144
                        then
1145
                                ( "$PACMAN" --deptest "${depends[@]}" || true ) | mapfile -t missing_depends
36✔
1146
                                if [[ ${#missing_depends[@]} != 0 ]]
12✔
1147
                                then
1148
                                        for dependency in "${missing_depends[@]}"
31✔
1149
                                        do
1150
                                                LogEnter '%s:\n' "$(Color M %q "$dependency")"
62✔
1151
                                                if "$PACMAN" --query --info "$dependency" > /dev/null 2>&1
31✔
1152
                                                then
1153
                                                        Log 'Already installed.\n' # Shouldn't happen, actually
×
1154
                                                elif "$PACMAN" --sync --info "$dependency" > /dev/null 2>&1
31✔
1155
                                                then
1156
                                                        Log 'Installing from repositories...\n'
24✔
1157
                                                        AconfInstallNative --asdeps "$dependency"
24✔
1158
                                                        Log 'Installed.\n'
24✔
1159
                                                else
1160
                                                        local installed=false
7✔
1161

1162
                                                        # Check if this package is provided by something in pacman repos.
1163
                                                        # `pacman -Si` will not give us that information,
1164
                                                        # however, `pacman -S` still works.
1165
                                                        AconfNeedProgram pacsift pacutils n
7✔
1166
                                                        AconfNeedProgram unbuffer expect n
7✔
1167
                                                        local providers
7✔
1168
                                                        providers=$(unbuffer pacsift --sync --exact --satisfies="$dependency")
14✔
1169
                                                        if [[ -n "$providers" ]]
7✔
1170
                                                        then
1171
                                                                Log 'Installing provider package from repositories...\n'
3✔
1172
                                                                AconfInstallNative --asdeps "$dependency"
3✔
1173
                                                                Log 'Installed.\n'
3✔
1174
                                                                installed=true
3✔
1175
                                                        fi
1176

1177
                                                        if ! $installed
7✔
1178
                                                        then
1179
                                                                Log 'Installing from AUR...\n'
4✔
1180
                                                                AconfMakePkg "$dependency" true
4✔
1181
                                                                Log 'Installed.\n'
4✔
1182
                                                        fi
1183
                                                fi
1184

1185
                                                LogLeave ''
31✔
1186
                                        done
1187
                                fi
1188
                        fi
1189

1190
                        LogLeave
22✔
1191

1192
                        local keys
22✔
1193
                        ( grep -E $'^\tvalidpgpkeys = ' "$infofile" || true ) | sed 's/^.* = \(.*\)$/\1/' | mapfile -t keys
87✔
1194
                        if [[ ${#keys[@]} != 0 ]]
22✔
1195
                        then
1196
                                LogEnter 'Checking PGP keys...\n'
1✔
1197

1198
                                local key
1✔
1199
                                for key in "${keys[@]}"
1✔
1200
                                do
1201
                                        export GNUPGHOME="$gnupg_home"
2✔
1202

1203
                                        if [[ ! -d "$GNUPGHOME" ]]
1✔
1204
                                        then
1205
                                                LogEnter 'Creating %s...\n' "$(Color C %s "$GNUPGHOME")"
2✔
1206
                                                mkdir --parents "$GNUPGHOME"
1✔
1207
                                                gpg --gen-key --batch <<EOF
1✔
1208
Key-Type: DSA
1✔
1209
Key-Length: 1024
1✔
1210
Name-Real: aconfmgr
1✔
1211
%no-protection
1✔
1212
EOF
1✔
1213
                                                LogLeave
1✔
1214
                                        fi
1✔
1215

1✔
1216
                                        LogEnter 'Adding key %s...\n' "$(Color Y %q "$key")"
2✔
1217
                                        #ParanoidConfirm ''
1✔
1218

1✔
1219
                                        local ok=false
1✔
1220
                                        local keyserver
1✔
1221
                                        for keyserver in keys.gnupg.net pgp.mit.edu pool.sks-keyservers.net keyserver.ubuntu.com # subkeys.pgp.net
1✔
1222
                                        do
1✔
1223
                                                LogEnter 'Trying keyserver %s...\n' "$(Color C %s "$keyserver")"
2✔
1224
                                                if gpg --keyserver "$keyserver" --recv-key "$key"
1✔
1225
                                                then
1✔
1226
                                                        ok=true
1✔
1227
                                                        LogLeave 'OK!\n'
1✔
1228
                                                        break
1✔
1229
                                                else
1✔
1230
                                                        LogLeave 'Error...\n'
1✔
1231
                                                fi
1✔
1232
                                        done
1✔
1233

1✔
1234
                                        if ! $ok
1✔
1235
                                        then
1✔
1236
                                                FatalError 'No keyservers succeeded.\n'
1✔
1237
                                        fi
1✔
1238

1✔
1239
                                        if [[ $EUID == 0 ]]
1✔
1240
                                        then
1✔
1241
                                                chmod 700 "$gnupg_home"
1✔
1242
                                                chown -R "$makepkg_user": "$gnupg_home"
1✔
1243
                                        fi
1✔
1244

1✔
1245
                                        LogLeave
1✔
1246
                                done
1✔
1247

1✔
1248
                                LogLeave
1✔
1249
                        fi
1✔
1250
                fi
1✔
1251
        done
1✔
1252

1✔
1253
        LogEnter 'Evaluating environment...\n'
22✔
1254
        local path
22✔
1255
        # shellcheck disable=SC2016
1✔
1256
        path=$(env -i sh -c 'source /etc/profile 1>&2 ; printf -- %s "$PATH"')
44✔
1257
        LogLeave
22✔
1258

1✔
1259
        LogEnter 'Building...\n'
22✔
1260
        (
1✔
1261
                cd "$pkg_dir"
22✔
1262
                mkdir --parents home
22✔
1263
                # Set CARGO_TARGET_DIR to avoid issues with Rust builds on mounted volumes
1✔
1264
                # This prevents "could not write output" errors when building Rust packages
1✔
1265
                local cargo_target_dir="/tmp/cargo-target-$$"
22✔
1266
                local args=(env -i "PATH=$path" "HOME=$PWD/home" "GNUPGHOME=$gnupg_home" "CARGO_TARGET_DIR=$cargo_target_dir" "${makepkg_opts[@]}")
44✔
1267

1✔
1268
                if [[ $EUID == 0 ]]
22✔
1269
                then
1✔
1270
                        chown -R "$makepkg_user": .
1✔
1271
                        setpriv --reuid="$makepkg_user" --regid="$makepkg_user" --clear-groups bash -c "GNUPGHOME=$(realpath ../../gnupg) $(printf ' %q' "${args[@]}")" 1>&2
1✔
1272

1✔
1273
                        if $install
1✔
1274
                        then
1✔
1275
                                local pkglist
1✔
1276
                                setpriv --reuid="$makepkg_user" --regid="$makepkg_user" --clear-groups bash -c "GNUPGHOME=$(realpath ../../gnupg) $(printf ' %q' "${args[@]}" --packagelist)" | mapfile -t pkglist
1✔
1277

1✔
1278
                                # Filter out packages that don't exist (e.g., debug packages not built by default)
1✔
1279
                                local existing_pkgs=()
1✔
1280
                                local skipped_pkgs=()
1✔
1281
                                local pkg
1✔
1282
                                for pkg in "${pkglist[@]}"
1✔
1283
                                do
1✔
1284
                                        if [[ -f "$pkg" ]]
1✔
1285
                                        then
1✔
1286
                                                existing_pkgs+=("$pkg")
1✔
1287
                                        else
1✔
1288
                                                skipped_pkgs+=("$pkg")
1✔
1289
                                        fi
1✔
1290
                                done
1✔
1291

1✔
1292
                                if [[ ${#skipped_pkgs[@]} -gt 0 ]]
1✔
1293
                                then
1✔
1294
                                        Log 'Skipping %s non-existent package(s) from packagelist:\n' "$(Color G "${#skipped_pkgs[@]}")"
1✔
1295
                                        for pkg in "${skipped_pkgs[@]}"
1✔
1296
                                        do
1✔
1297
                                                Log '  %s\n' "$(Color M "%q" "$(basename "$pkg")")"
1✔
1298
                                        done
1✔
1299
                                fi
1✔
1300

1✔
1301
                                if [[ ${#existing_pkgs[@]} -eq 0 ]]
1✔
1302
                                then
1✔
1303
                                        Log 'Warning: No packages to install (all were filtered out)\n'
1✔
1304
                                elif $asdeps
1✔
1305
                                then
1✔
1306
                                        "${pacman_opts[@]}" --upgrade --asdeps "${existing_pkgs[@]}"
1✔
1307
                                else
1✔
1308
                                        "${pacman_opts[@]}" --upgrade "${existing_pkgs[@]}"
1✔
1309
                                fi
1✔
1310
                        fi
1✔
1311
                else
1✔
1312
                        if $asdeps
22✔
1313
                        then
1✔
1314
                                args+=(--asdeps)
6✔
1315
                        fi
1✔
1316

1✔
1317
                        if $install
22✔
1318
                        then
1✔
1319
                                args+=(--install)
19✔
1320
                        fi
22✔
1321

22✔
1322
                        "${args[@]}" 1>&2
22✔
1323
                fi
1✔
1324
        )
1✔
1325
        LogLeave
22✔
1326

1✔
1327
        LogLeave
22✔
1328
}
1✔
1329

1✔
1330
function AconfInstallNative() {
1✔
1331
        local asdeps=false asdeps_arr=()
66✔
1332
        if [[ "$1" == --asdeps ]]
33✔
1333
        then
1✔
1334
                asdeps=true
28✔
1335
                asdeps_arr=(--asdeps)
28✔
1336
                shift
28✔
1337
        fi
1✔
1338

1✔
1339
        local target_packages=("$@")
66✔
1340
        if [[ $prompt_mode == never ]]
33✔
1341
        then
1✔
1342
                # Some prompts default to 'no'
1✔
1343
                ( yes || true ) | sudo "${pacman_opts[@]}" --confirm --sync "${asdeps_arr[@]}" "${target_packages[@]}"
1✔
1344
        else
1✔
1345
                sudo "${pacman_opts[@]}" --sync "${asdeps_arr[@]}" "${target_packages[@]}"
33✔
1346
        fi
1✔
1347
}
1✔
1348

1✔
1349
function AconfInstallForeign() {
1✔
1350
        local asdeps=false asdeps_arr=()
32✔
1351
        if [[ "$1" == --asdeps ]]
16✔
1352
        then
1✔
1353
                asdeps=true
2✔
1354
                asdeps_arr=(--asdeps)
2✔
1355
                shift
2✔
1356
        fi
1✔
1357

1✔
1358
        local target_packages=("$@")
32✔
1359

1✔
1360
        DetectAurHelper
16✔
1361

1✔
1362
        case "$aur_helper" in
16✔
1363
                aurman)
1✔
1364
                        RunExternal "${aurman_opts[@]}" --sync --aur "${asdeps_arr[@]}" "${target_packages[@]}"
1✔
1365
                        ;;
1✔
1366
                pacaur)
1✔
1367
                        RunExternal "${pacaur_opts[@]}" --sync --aur "${asdeps_arr[@]}" "${target_packages[@]}"
3✔
1368
                        ;;
1✔
1369
                yaourt)
1✔
1370
                        RunExternal "${yaourt_opts[@]}" --sync --aur "${asdeps_arr[@]}" "${target_packages[@]}"
2✔
1371
                        ;;
1✔
1372
                yay)
1✔
1373
                        RunExternal "${yay_opts[@]}" --sync --aur "${asdeps_arr[@]}" "${target_packages[@]}"
2✔
1374
                        ;;
1✔
1375
                paru)
1✔
1376
                        RunExternal "${paru_opts[@]}" --sync --aur "${asdeps_arr[@]}" "${target_packages[@]}"
2✔
1377
                        ;;
1✔
1378
                aura)
1✔
1379
                        RunExternal "${aura_opts[@]}" -A "${asdeps_arr[@]}" "${target_packages[@]}"
2✔
1380
                        ;;
1✔
1381
                makepkg)
1✔
1382
                        local package
10✔
1383
                        for package in "${target_packages[@]}"
10✔
1384
                        do
1✔
1385
                                AconfMakePkg "$package" "$asdeps"
10✔
1386
                        done
1✔
1387
                        ;;
1✔
1388
                *)
1✔
1389
                        Log 'Error: unknown AUR helper %q\n' "$aur_helper"
1✔
1390
                        false
1✔
1391
                        ;;
1✔
1392
        esac
1✔
1393
}
1✔
1394

1✔
1395
function AconfNeedProgram() {
1✔
1396
        local program="$1" # program that needs to be in PATH
277✔
1397
        local package="$2" # package the program is available in
277✔
1398
        local foreign="$3" # whether this is a foreign package
279✔
1399

1✔
1400
        if ! hash "$program" 2> /dev/null
279✔
1401
        then
1✔
1402
                if [[ $foreign == y ]]
3✔
1403
                then
1✔
1404
                        LogEnter 'Installing foreign dependency %s:\n' "$(Color M %q "$package")"
3✔
1405
                        ParanoidConfirm ''
2✔
1406
                        AconfInstallForeign --asdeps "$package"
2✔
1407
                else
1✔
1408
                        LogEnter 'Installing native dependency %s:\n' "$(Color M %q "$package")"
3✔
1409
                        ParanoidConfirm ''
2✔
1410
                        AconfInstallNative --asdeps "$package"
2✔
1411
                fi
1✔
1412
                LogLeave 'Installed.\n'
3✔
1413
        fi
1✔
1414
}
1✔
1415

1✔
1416
# Get the path to the package file (.pkg.tar.*) for the specified package.
1✔
1417
# Download or build the package if necessary.
1✔
1418
function AconfNeedPackageFile() {
1✔
1419
        set -e
31✔
1420
        local package="$1"
31✔
1421

1✔
1422
        local info foreign
31✔
1423
        if info="$(LC_ALL=C "$PACMAN" --query --info "$package")"
93✔
1424
        then
1✔
1425
                if "$PACMAN" --query --quiet --foreign "$package" > /dev/null
15✔
1426
                then
1✔
1427
                        foreign=true
7✔
1428
                else
1✔
1429
                        foreign=false
9✔
1430
                fi
1✔
1431
        else
1✔
1432
                if info="$(LC_ALL=C "$PACMAN" --sync --info "$package")"
48✔
1433
                then
1✔
1434
                        foreign=false
6✔
1435
                else
1✔
1436
                        foreign=true
11✔
1437
                fi
1✔
1438
        fi
1✔
1439

1✔
1440
        local version='' architecture='' filemask_precise filemask_any
31✔
1441
        if [[ -n "$info" ]]
31✔
1442
        then
1✔
1443
                version="$(grep '^Version' <<< "$info" | sed 's/^.* : //g')"
60✔
1444
                architecture="$(grep '^Architecture' <<< "$info" | sed 's/^.* : //g')"
60✔
1445
                filemask_precise=$(printf "%q-%q-%q.pkg.*" "$package" "$version" "$architecture")
40✔
1446
        fi
1✔
1447
        filemask_any=$(printf "%q-*-*.pkg.*" "$package")
62✔
1448

1✔
1449
        # try without downloading first
1✔
1450
        local downloaded
31✔
1451
        for downloaded in false true
40✔
1452
        do
1✔
1453
                local precise
40✔
1454
                for precise in true false
60✔
1455
                do
1✔
1456
                        # if we don't have the exact version, we can only do non-precise
1✔
1457
                        if $precise && [[ -z "$version" ]]
100✔
1458
                        then
1✔
1459
                                continue
15✔
1460
                        fi
1✔
1461

1✔
1462
                        local filemask
45✔
1463
                        if $precise
45✔
1464
                        then
1✔
1465
                                filemask=$filemask_precise
25✔
1466
                        else
1✔
1467
                                filemask=$filemask_any
20✔
1468
                        fi
1✔
1469

1✔
1470
                        local dirs=()
90✔
1471
                        if $foreign
45✔
1472
                        then
1✔
1473
                                DetectAurHelper
24✔
1474
                                local -A tried_helper=()
48✔
1475

1✔
1476
                                local helper
24✔
1477
                                for helper in "$aur_helper" "${aur_helpers[@]}"
192✔
1478
                                do
1✔
1479
                                        if [[ ${tried_helper[$helper]+x} ]]
192✔
1480
                                        then
1✔
1481
                                                continue
24✔
1482
                                        fi
1✔
1483
                                        tried_helper[$helper]=y
168✔
1484

1✔
1485
                                        case "$helper" in
168✔
1486
                                                aurman)
1✔
1487
                                                        dirs+=("${XDG_CACHE_HOME:-$HOME/.cache}/aurman/$package")
24✔
1488
                                                        ;;
1✔
1489
                                                pacaur)
1✔
1490
                                                        dirs+=("${XDG_CACHE_HOME:-$HOME/.cache}/pacaur/$package")
24✔
1491
                                                        ;;
1✔
1492
                                                yaourt)
1✔
1493
                                                        # yaourt does not save .pkg.xz files
1✔
1494
                                                        ;;
1✔
1495
                                                yay)
1✔
1496
                                                        dirs+=("${XDG_CACHE_HOME:-$HOME/.cache}/yay/$package")
24✔
1497
                                                        ;;
1✔
1498
                                                paru)
1✔
1499
                                                        dirs+=("${XDG_CACHE_HOME:-$HOME/.cache}/paru/clone/$package")
24✔
1500
                                                        ;;
1✔
1501
                                                aura)
1✔
1502
                                                        dirs+=("${XDG_CACHE_HOME:-$HOME/.cache}/aura/cache")
24✔
1503
                                                        ;;
1✔
1504
                                                makepkg)
1✔
1505
                                                        dirs+=("$aur_dir"/"$package")
24✔
1506
                                                        ;;
1✔
1507
                                                *)
1✔
1508
                                                        Log 'Error: unknown AUR helper %q\n' "$aur_helper"
1✔
1509
                                                        false
1✔
1510
                                                        ;;
1✔
1511
                                        esac
1✔
1512
                                done
1✔
1513
                        else
1✔
1514
                                local dir
22✔
1515
                                ( LC_ALL=C pacman --verbose 2>/dev/null || true ) \
64✔
1516
                                        | sed -n 's/^Cache Dirs: \(.*\)$/\1/p' \
22✔
1517
                                        | sed 's/  /\n/g' \
22✔
1518
                                        | while read -r dir
64✔
1519
                                do
1✔
1520
                                        if [[ -n "$dir" ]]
43✔
1521
                                        then
1✔
1522
                                                dirs+=("$dir")
22✔
1523
                                        fi
1✔
1524
                                done
1✔
1525
                        fi
1✔
1526

1✔
1527
                        local files=()
90✔
1528
                        local dir
45✔
1529
                        for dir in "${dirs[@]}"
165✔
1530
                        do
1✔
1531
                                if sudo test -d "$dir"
165✔
1532
                                then
1✔
1533
                                        sudo find "$dir" -type f -name "$filemask" -not -name '*.sig' -print0 | \
39✔
1534
                                                while read -r -d $'\0' file
70✔
1535
                                                do
1✔
1536
                                                        files+=("$file")
31✔
1537
                                                done
1✔
1538
                                fi
1✔
1539
                        done
1✔
1540

1✔
1541
                        local file
45✔
1542
                        for file in "${files[@]}"
31✔
1543
                        do
1✔
1544
                                local correct
31✔
1545
                                if $precise
31✔
1546
                                then
1✔
1547
                                        correct=true
20✔
1548
                                else
1✔
1549
                                        local pkgname
11✔
1550
                                        pkgname=$(bsdtar -x --to-stdout --file "$file" .PKGINFO | \
1✔
1551
                                                                  sed -n 's/^pkgname = \(.*\)$/\1/p')
33✔
1552
                                        if [[ "$pkgname" == "$package" ]]
11✔
1553
                                        then
1✔
1554
                                                correct=true
11✔
1555
                                        else
1✔
1556
                                                correct=false
1✔
1557
                                        fi
1✔
1558
                                fi
1✔
1559

1✔
1560
                                if $correct
31✔
1561
                                then
1✔
1562
                                        printf '%s' "$file"
31✔
1563
                                        return
31✔
1564
                                fi
1✔
1565
                        done
1✔
1566
                done
1✔
1567

1✔
1568
                if $downloaded
10✔
1569
                then
1✔
1570
                        Log 'Unable to find package file for package %s!\n' "$(Color M %q "$package")"
1✔
1571
                        Exit 1
1✔
1572
                else
1✔
1573
                        if $foreign
10✔
1574
                        then
1✔
1575
                                LogEnter 'Building foreign package %s\n' "$(Color M %q "$package")"
11✔
1576
                                ParanoidConfirm ''
6✔
1577

1✔
1578
                                local helper
6✔
1579
                                for helper in "$aur_helper" "${aur_helpers[@]}"
13✔
1580
                                do
1✔
1581
                                        case "$helper" in
13✔
1582
                                                aurman)
1✔
1583
                                                        # aurman does not have a --makepkg option
1✔
1584
                                                        ;;
1✔
1585
                                                pacaur)
1✔
1586
                                                        if command -v "${pacaur_opts[0]}" > /dev/null
4✔
1587
                                                        then
1✔
1588
                                                                RunExternal "${pacaur_opts[@]}" --makepkg --aur --makepkg "$package" 1>&2
3✔
1589
                                                                break
3✔
1590
                                                        fi
1✔
1591
                                                        ;;
1✔
1592
                                                yaourt)
1✔
1593
                                                        # yaourt does not save .pkg.xz files
1✔
1594
                                                        continue
3✔
1595
                                                        ;;
1✔
1596
                                                yay)
1✔
1597
                                                        # yay does not have a --makepkg option
1✔
1598
                                                        continue
2✔
1599
                                                        ;;
1✔
1600
                                                paru)
1✔
1601
                                                        # paru does not have a --makepkg option
1✔
1602
                                                        continue
2✔
1603
                                                        ;;
1✔
1604
                                                aura)
1✔
1605
                                                        # aura does not have a --makepkg option
1✔
1606
                                                        continue
2✔
1607
                                                        ;;
1✔
1608
                                                makepkg)
1✔
1609
                                                        AconfMakePkg --noinstall "$package"
4✔
1610
                                                        break
4✔
1611
                                                        ;;
1✔
1612
                                                *)
1✔
1613
                                                        Log 'Error: unknown AUR helper %q\n' "$aur_helper"
1✔
1614
                                                        false
1✔
1615
                                                        ;;
1✔
1616
                                        esac
1✔
1617
                                done
1✔
1618

1✔
1619
                                LogLeave
6✔
1620
                        else
1✔
1621
                                LogEnter "Downloading package %s (%s) to pacman's cache\\n" "$(Color M %q "$package")" "$(Color C %s "$filemask_precise")"
13✔
1622
                                ParanoidConfirm ''
5✔
1623
                                sudo "$PACMAN" --sync --download --nodeps --nodeps --noconfirm "$package" 1>&2
5✔
1624
                                LogLeave
5✔
1625
                        fi
1✔
1626
                fi
1✔
1627
        done
1✔
1628
}
1✔
1629

1✔
1630
# Extract the original file from a package to stdout
1✔
1631
function AconfGetPackageOriginalFile() {
1✔
1632
        local package="$1" # Package to extract the file from
26✔
1633
        local file="$2" # Absolute path to file in package
26✔
1634

1✔
1635
        local package_file
26✔
1636
        package_file="$(AconfNeedPackageFile "$package")"
52✔
1637

1✔
1638
        local args=(bsdtar -x --to-stdout --file "$package_file" "${file/\//}")
52✔
1639
        if [[ -r "$package_file" ]]
26✔
1640
        then
24✔
1641
                "${args[@]}"
25✔
1642
        else
1✔
1643
                sudo "${args[@]}"
2✔
1644
        fi
1✔
1645
}
1✔
1646

1✔
1647
function AconfRestoreFile() {
1✔
1648
        local package=$1
6✔
1649
        local file=$2
6✔
1650

1✔
1651
        local package_file
6✔
1652
        package_file="$(AconfNeedPackageFile "$package")"
11✔
1653

1✔
1654
        # If we are restoring a directory, it may be non-empty.
1✔
1655
        # Extract the object to a temporary location first.
1✔
1656
        local tmp_base=${tmp_dir:?}/dir-props
6✔
1657
        sudo rm -rf "$tmp_base"
6✔
1658

1✔
1659
        mkdir -p "$tmp_base"
6✔
1660
        local tmp_file="$tmp_base""$file"
6✔
1661
        sudo tar x --directory "$tmp_base" --file "$package_file" --no-recursion "${file/\//}"
6✔
1662

1✔
1663
        AconfReplace "$tmp_file" "$file"
6✔
1664
        sudo rm -rf "$tmp_base"
6✔
1665
}
1✔
1666

1✔
1667
# Move filesystem object at $1 to $2, replacing any existing one.
1✔
1668
# Attempt to do so atomically, when possible.
1✔
1669
# Do the right thing when filesystem objects differ, but never
1✔
1670
# recursively remove directories (copy their attributes instead).
1✔
1671
function AconfReplace() {
1✔
1672
        local src=$1
6✔
1673
        local dst=$2
6✔
1674

1✔
1675
        # Try direct mv first
1✔
1676
        if ! sudo mv --no-target-directory "$src" "$dst" 2>/dev/null
6✔
1677
        then
1✔
1678
                # Direct mv failed - directory or object type mismatch
1✔
1679
                if sudo rm --force --dir "$dst" 2>/dev/null
3✔
1680
                then
1✔
1681
                        # Deleted target successfully, now overwrite it
1✔
1682
                        sudo mv --no-target-directory "$src" "$dst"
3✔
1683
                else
1✔
1684
                        # rm failed - likely a non-empty directory; copy
1✔
1685
                        # attributes only
1✔
1686
                        sudo chmod --reference="$src" "$dst"
1✔
1687
                        sudo chown --reference="$src" "$dst"
1✔
1688
                        sudo touch --reference="$src" "$dst"
1✔
1689
                fi
1✔
1690
        fi
1✔
1691
}
1✔
1692

1✔
1693
####################################################################################################
1✔
1694

1✔
1695
prompt_mode=normal # never / normal / paranoid
90✔
1696

1✔
1697
function Confirm() {
1✔
1698
        local detail_func="$1"
1✔
1699

1✔
1700
        if [[ $prompt_mode == never ]]
1✔
1701
        then
1✔
1702
                return
1✔
1703
        fi
1✔
1704

1✔
1705
        while true
1✔
1706
        do
1✔
1707
                if [[ -n "$detail_func" ]]
1✔
1708
                then
1✔
1709
                        Log 'Proceed? [Y/n/d] '
1✔
1710
                else
1✔
1711
                        Log 'Proceed? [Y/n] '
1✔
1712
                fi
1✔
1713
                read -r -n 1 answer < /dev/tty
1✔
1714
                echo 1>&2
1✔
1715
                case "$answer" in
1✔
1716
                        Y|y|'')
1✔
1717
                                return
1✔
1718
                                ;;
1✔
1719
                        N|n)
1✔
1720
                                Log '%s\n' "$(Color R "User abort")"
1✔
1721
                                Exit 1
1✔
1722
                                ;;
1✔
1723
                        D|d)
1✔
1724
                                $detail_func
1✔
1725
                                continue
1✔
1726
                                ;;
1✔
1727
                        *)
1✔
1728
                                continue
1✔
1729
                                ;;
1✔
1730
                esac
1✔
1731
        done
1✔
1732
}
1✔
1733

1✔
1734
function ParanoidConfirm() {
1✔
1735
        if [[ $prompt_mode == paranoid ]]
101✔
1736
        then
1✔
1737
                Confirm "$@"
101✔
1738
        fi
1✔
1739
}
1✔
1740

1✔
1741
####################################################################################################
1✔
1742

1✔
1743
log_indent=:
90✔
1744

1✔
1745
function Log() {
1✔
1746
        if [[ "$#" != 0 && -n "$1" ]]
139,610✔
1747
        then
1✔
1748
                local fmt="$1"
69,060✔
1749
                shift
69,073✔
1750

1✔
1751
                if [[ -z $ANSI_clear_line ]]
68,921✔
1752
                then
1✔
1753
                        # Replace carriage returns in format string with newline
1✔
1754
                        # when colors are disabled. This avoids systemd's journal
1✔
1755
                        # from showing such lines as [# blob data].
1✔
1756

1✔
1757
                        fmt=${fmt//\\r/\\n} # Replace the '\r' sequence
1✔
1758
                                            # (backslash-r) , not actual carriage
1✔
1759
                                            # returns.
1✔
1760
                fi
1✔
1761

1✔
1762
                printf "${ANSI_clear_line}${ANSI_color_B}%s ${ANSI_color_W}${fmt}${ANSI_reset}" "$log_indent" "$@" 1>&2
68,843✔
1763
        fi
1✔
1764
}
1✔
1765

1✔
1766
function LogEnter() {
1✔
1767
        Log "$@"
3,295✔
1768
        log_indent=$log_indent:
3,251✔
1769
}
1✔
1770

1✔
1771
function LogLeave() {
1✔
1772
        if [[ $# == 0 ]]
3,275✔
1773
        then
1✔
1774
                Log 'Done.\n'
2,101✔
1775
        else
1✔
1776
                Log "$@"
1,173✔
1777
        fi
1✔
1778

1✔
1779
        log_indent=${log_indent::-1}
3,281✔
1780
}
1✔
1781

1✔
1782
function ConfigWarning() {
1✔
1783
        Log '%s: '"$1" "$(Color Y "Warning")" "${@:2}"
9✔
1784
        printf W >> "$output_dir"/warnings
5✔
1785
}
1✔
1786

1✔
1787
function FatalError() {
1✔
1788
        Log "$@"
1✔
1789
        false
1✔
1790
        # if we're here, errexit is not set
1✔
1791
        Log 'Continuing after error. This is a bug, please report it.\n'
1✔
1792
        Exit 1
1✔
1793
}
1✔
1794

1✔
1795
function Color() {
1✔
1796
        local var="ANSI_color_$1"
65,179✔
1797
        printf -- "%s" "${!var}"
65,113✔
1798
        shift
65,158✔
1799
        # shellcheck disable=2059
1✔
1800
        printf -- "$@"
64,995✔
1801
        printf -- "%s" "${ANSI_color_W}"
64,939✔
1802
}
1✔
1803

1✔
1804
# The ANSI_color_* variables are looked up by name:
1✔
1805
# shellcheck disable=2034
1✔
1806
function DisableColor() {
1✔
1807
        ANSI_color_R=
1✔
1808
        ANSI_color_G=
1✔
1809
        ANSI_color_Y=
1✔
1810
        ANSI_color_B=
1✔
1811
        ANSI_color_M=
1✔
1812
        ANSI_color_C=
1✔
1813
        ANSI_color_W=
1✔
1814
        ANSI_reset=
1✔
1815
        ANSI_clear_line=
1✔
1816
}
1✔
1817

1✔
1818
####################################################################################################
1✔
1819

1✔
1820
# shellcheck disable=SC2329  # This function is invoked indirectly via trap
1✔
1821
function OnError() {
1✔
1822
        trap '' EXIT ERR
1✔
1823

1✔
1824
        LogEnter '%s! Stack trace:\n' "$(Color R "Fatal error")"
1✔
1825

1✔
1826
        local frame=0 str
1✔
1827
        # shellcheck disable=SC2086  # frame is a controlled integer, no risk of word splitting
1✔
1828
        while str=$(caller $frame)
1✔
1829
        do
1✔
1830
                if [[ $str =~ ^([^\ ]*)\ ([^\ ]*)\ (.*)$ ]]
1✔
1831
                then
1✔
1832
                        Log '%s:%s [%s]\n' "$(Color C "%q" "${BASH_REMATCH[3]}")" "$(Color G "%q" "${BASH_REMATCH[1]}")" "$(Color Y "%q" "${BASH_REMATCH[2]}")"
1✔
1833
                else
1✔
1834
                        Log '%s\n' "$str"
1✔
1835
                fi
1✔
1836

1✔
1837
                frame=$((frame+1))
1✔
1838
        done
1✔
1839

1✔
1840
        LogLeave ''
1✔
1841

1✔
1842
        if [[ -d "$tmp_dir" ]]
1✔
1843
        then
1✔
1844
                local df dir
1✔
1845
                df=$(($(stat -f --format="%a*%S" "$tmp_dir")))
1✔
1846
                if ! dir="$(realpath "$(dirname "$tmp_dir")" 2> /dev/null)"
1✔
1847
                then
1✔
1848
                        dir="$(dirname "$tmp_dir")"
1✔
1849
                fi
1✔
1850
                if [[ $df -lt $warn_tmp_df_threshold ]]
1✔
1851
                then
1✔
1852
                        LogEnter 'Probable cause: low disk space (%s bytes) in %s. Suggestions:\n' "$(Color G %s "$df")" "$(Color C %q "$dir")"
1✔
1853
                        Log '- Ignore more files and directories using %s directives;\n' "$(Color Y IgnorePath)"
1✔
1854
                        Log '- Free up more space in %s;\n' "$(Color C %q "$dir")"
1✔
1855
                        Log '- Set %s to another location before invoking %s.\n' "$(Color Y \$TMPDIR)" "$(Color Y aconfmgr)"
1✔
1856
                        LogLeave ''
1✔
1857
                fi
1✔
1858
        fi
1✔
1859

1✔
1860
        # Ensure complete abort when inside a string expansion
1✔
1861
        exit 1
1✔
1862
}
1✔
1863
trap OnError EXIT ERR
90✔
1864

1✔
1865
function Exit() {
1✔
1866
        trap '' EXIT ERR
89✔
1867
        exit "${1:-0}"
89✔
1868
}
1✔
1869

1✔
1870
####################################################################################################
1✔
1871

1✔
1872
# Print an array, one element per line (assuming IFS starts with \n).
1✔
1873
function PrintArray() {
1✔
1874
        local name="$1" # Name of the global variable containing the array
1,174✔
1875
        local size
1,174✔
1876

1✔
1877
        size="$(eval "echo \${#${name}""[*]}")"
3,522✔
1878
        if [[ $size != 0 ]]
1,174✔
1879
        then
1✔
1880
                eval "echo \"\${${name}[*]}\""
1,318✔
1881
        fi
1✔
1882
}
1✔
1883

1✔
1884
# Ditto, but terminate elements with a NUL.
1✔
1885
function Print0Array() {
1✔
1886
        local name="$1" # Name of the global variable containing the array
1,089✔
1887

1✔
1888
        eval "$(cat <<EOF
3,267✔
1889
        if [[ \${#${name}[*]} != 0 ]]
3,226✔
1890
        then
737✔
1891
                local item
6,145✔
1892
                for item in "\${${name}[@]}"
3,226✔
1893
                do
6,129✔
1894
                        printf '%s\\0' "\$item"
3,226✔
1895
                done
3,226✔
1896
        fi
3,226✔
1897
EOF
3,226✔
1898
)"
×
1899
}
1900

1901
# Ditto, but shell-escape array elements.
1902
# shellcheck disable=SC2329  # This is a utility function called indirectly
1903
function PrintQArray() {
1904
        local name="$1" # Name of the global variable containing the array
909✔
1905
        local size
909✔
1906

1907
        size="$(eval "echo \${#${name}""[*]}")"
2,727✔
1908
        if [[ $size != 0 ]]
909✔
1909
        then
1910
                eval "printf -- %q \"\${${name}[0]}\""
8✔
1911
                if [[ $size -gt 1 ]]
4✔
1912
                then
1913
                        eval "printf -- ' %q' \"\${${name}[@]:1}\""
2✔
1914
                fi
1915
        fi
1916
}
1917

1918
if [[ $EUID == 0 ]]
90✔
1919
then
1920
        function sudo() { "$@" ; }
1921
fi
1922

1923
# Run external bash script.
1924
# No-op, except when running under the test suite with bashcov,
1925
# in which case it does not propagate bashcov to the invoked script.
1926
# Unlike `env -i`, does not nuke the environment.
1927
function RunExternal() {
1928
        env -u SHELLOPTS -u PS4 -u SHLVL -u BASH_XTRACEFD "$@"
9✔
1929
}
1930

1931
# cat a file; if it's not readable, cat via sudo.
1932
# shellcheck disable=SC2329  # This is a utility function called indirectly
1933
function SuperCat() {
1934
        local file="$1"
1✔
1935

1936
        if [[ -r "$1" ]]
1✔
1937
        then
1938
                cat "$1"
1✔
1939
        else
1940
                sudo cat "$1"
×
1941
        fi
1942
}
1943

1944
if ! ( empty_array=() ; : "${empty_array[@]}" )
180✔
1945
then
1946
        # Old bash versions treat substitution of an empty array
1947
        # synonymous with substituting an unset variable, signaling an
1948
        # error in -u mode and stopping the script in -e mode. We
1949
        # generally want both of these enabled to catch bugs early and
1950
        # fail fast, but making every array substitution conditional on
1951
        # whether it is empty or not is unreasonably onerous, so just
1952
        # disable those checks in old bash versions - it is then up to the
1953
        # test suite ran against newer bash versions to ensure code
1954
        # correctness.
1955

1956
        Log '%s: Old bash detected, disabling unset variable checking.\n' "$(Color Y "Warning")"
×
1957
        set +u
×
1958
fi
1959

1960
: # include in coverage
90✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc