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

CyberShadow / aconfmgr / 661

22 Dec 2025 03:28PM UTC coverage: 79.384% (-14.3%) from 93.708%
661

push

github

web-flow
Merge b1f7495b1 into 8755dc2ab

3223 of 4060 relevant lines covered (79.38%)

293.79 hits per line

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

58.0
/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}
6,557✔
10

11
output_dir="$tmp_dir"/output
6,557✔
12
system_dir="$tmp_dir"/system # Current system configuration, to be compared against the output directory
6,557✔
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 ]]
6,557✔
19
then
20
        aur_dir="$tmp_dir"-aur
×
21
else
22
        aur_dir="$tmp_dir"/aur
6,557✔
23
fi
24

25
default_file_mode=644
6,557✔
26

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

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

41
aconfmgr_action=
6,556✔
42
aconfmgr_action_args=()
6,556✔
43

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

46
# Defaults
47

48
# Initial ignore path list.
49
# Can be appended to using the IgnorePath helper.
50
ignore_paths=(
6,556✔
51
    '/dev'
6,556✔
52
    '/home'
6,556✔
53
    '/media'
6,556✔
54
    '/mnt'
6,556✔
55
    '/proc'
6,556✔
56
    '/root'
6,556✔
57
    '/run'
6,556✔
58
    '/sys'
6,556✔
59
    '/tmp'
6,556✔
60
    # '/var/.updated'
6,556✔
61
    '/var/cache'
6,556✔
62
    # '/var/lib'
6,556✔
63
    # '/var/lock'
6,556✔
64
    # '/var/log'
6,556✔
65
    # '/var/spool'
6,556✔
66
)
6,556✔
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=(
6,556✔
72
        /etc/passwd
6,556✔
73
        /etc/group
6,556✔
74
        /etc/pacman.conf
6,556✔
75
        /etc/pacman.d/mirrorlist
6,556✔
76
        /etc/makepkg.conf
6,556✔
77
)
6,556✔
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
6,556✔
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
6,556✔
92
warn_file_count_threshold=1000        # Warn on finding this many stray files
6,554✔
93
warn_tmp_df_threshold=$((1024*1024))  # Warn on error if free space in $tmp_dir is below this
6,554✔
94

95
makepkg_user=nobody # when running as root
6,553✔
96

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

99
function LogLeaveDirStats() {
100
        local dir="$1"
130✔
101
        Log 'Finalizing...\r'
130✔
102
        LogLeave 'Done (%s native packages, %s foreign packages, %s files).\n'        \
1,040✔
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
6,552✔
109

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

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

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

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

133
        # Configuration
134

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

137
        typeset -ag ignore_packages=()
152✔
138
        typeset -ag ignore_foreign_packages=()
152✔
139
        typeset -Ag used_files
76✔
140

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

155
        if $lint_config
76✔
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 ]]
76✔
179
        then
180
                LogLeaveDirStats "$output_dir"
58✔
181
        else
182
                LogLeave 'Done (configuration not found).\n'
18✔
183
        fi
184
}
185

186
skip_inspection=n
6,552✔
187
skip_checksums=n
6,553✔
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
72✔
202
        local -n ignore_args_var=$ignore_args_varname
72✔
203
        shift
72✔
204
        local ignore_paths=("$@")
144✔
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=()
144✔
212

213
        local ignore_path
72✔
214
        for ignore_path in "${ignore_paths[@]}"
740✔
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_/\ .*-] ]]
740✔
220
                then
221
                        ignore_args_var+=(-wholename "$ignore_path" -o)
5✔
222
                else
223
                        simple_ignore_paths+=("${ignore_path}")
735✔
224
                fi
225
        done
226

227
        if [ ${#simple_ignore_paths[@]} -ne 0 ]
72✔
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
72✔
232
                echo -n "${simple_ignore_paths[*]}" | \
72✔
233
                        sed 's|[^*abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_/ ]|[&]|g; s|\*|.*|g' | \
72✔
234
                        mapfile -t ignore_regexps
72✔
235

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

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

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

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

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

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

261
        ### Packages
262

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

268
        ### Files
269

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

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

277
        # Stray files
278

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

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

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

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

292
        AconfNeedProgram gawk gawk n
72✔
293

294
        # Progress display - only show file names once per second
295
        local progress_fd
72✔
296
        exec {progress_fd}> \
72✔
297
                 >( gawk '
72✔
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
        }
144✔
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
72✔
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 /                                                                        \
72✔
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                                \
72✔
338
                        | ( grep                                                                \
×
339
                                        --null --null-data                                \
144✔
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
240✔
350
                do
351
                        local file action
169✔
352
                        file=${line:1}
169✔
353
                        action=${line:0:1}
169✔
354

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

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

367
                                        if [[ $stray_file_count -eq $warn_file_count_threshold ]]
130✔
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"
41✔
400
                                        while [[ -n "$path" ]]
110✔
401
                                        do
402
                                                ignored_dirs[$path]=y
71✔
403
                                                path=${path%/*}
70✔
404
                                        done
405
                                        ;;
406
                        esac
407
                done
408

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

411
        exec {progress_fd}<&-
71✔
412

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

415
        local file
69✔
416
        for file in "${found_files[@]}"
130✔
417
        do
418
                if [[ -z "${ignored_dirs[$file]+x}" ]]
130✔
419
                then
420
                        local path="$file"
101✔
421
                        while [[ -n "$path" ]]
209✔
422
                        do
423
                                unset "ignored_dirs[\$path]"
110✔
424
                                path=${path%/*}
109✔
425
                        done
426
                fi
427
        done
428

429
        LogLeave
66✔
430

431
        # Modified files
432

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

435
        AconfNeedProgram paccheck pacutils n
66✔
436
        AconfNeedProgram unbuffer expect n
66✔
437
        local modified_file_count=0
66✔
438
        local -A saw_file
65✔
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
131✔
443

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

446
        local paccheck_opts=(unbuffer paccheck --files --file-properties --backup --noupgrade)
135✔
447
        if [[ $skip_checksums == n ]]
69✔
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'
134✔
452
                then
453
                        paccheck_opts+=(--sha256sum)
454
                else
455
                        paccheck_opts+=(--md5sum)
69✔
456
                fi
457
        fi
458

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

469
                                local ignored=n
112✔
470
                                local ignore_path
112✔
471
                                for ignore_path in "${ignore_paths[@]}"
1,578✔
472
                                do
473
                                        # shellcheck disable=SC2053
474
                                        if [[ "$file" == $ignore_path ]]
1,578✔
475
                                        then
476
                                                ignored=y
14✔
477
                                                break
14✔
478
                                        fi
479
                                done
480

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

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

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

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

527
                                local ignored=n
8✔
528
                                local ignore_path
8✔
529
                                for ignore_path in "${ignore_paths[@]}"
108✔
530
                                do
531
                                        # shellcheck disable=SC2053
532
                                        if [[ "$file" == $ignore_path ]]
107✔
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")"
9✔
545
                                printf '%s\t%s\t%q\n' "deleted" "y" "$file" >> "$system_dir"/file-props.txt
4✔
546
                                printf '%s\0%s\0' "$file" "$package" >> "$tmp_dir"/file-owners
5✔
547
                        elif [[ $line =~ ^warning:\ (.*):\ \'(.*)\'\ read\ error\ \(No\ such\ file\ or\ directory\)$ ]]
297✔
548
                        then
549
                                local package="${BASH_REMATCH[1]}"
205✔
550
                                local file="${BASH_REMATCH[2]}"
205✔
551
                                # Ignore
552
                        elif [[ $line =~ ^(.*):\ all\ files\ match\ (database|mtree|mtree\ md5sums|mtree\ sha256sums)$ ]]
92✔
553
                        then
554
                                local package="${BASH_REMATCH[1]}"
92✔
555
                                Log '%s...\r' "$(Color M "%q" "$package")"
184✔
556
                                #echo "Now at ${BASH_REMATCH[1]}"
557
                        else
558
                                Log 'Unknown paccheck output line: %s\n' "$(Color Y "%q" "$line")"
×
559
                        fi
560
                done
561
        LogLeave 'Done (%s modified files).\n' "$(Color G %s $modified_file_count)"
144✔
562

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

565
        typeset -a found_file_types found_file_sizes found_file_modes found_file_owners found_file_groups
72✔
566
        if [[ ${#found_files[*]} == 0 ]]
72✔
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
288✔
571
                Log 'Reading file sizes...\n'  ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%s | mapfile -t  found_file_sizes
288✔
572
                Log 'Reading file modes...\n'  ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%a | mapfile -t  found_file_modes
288✔
573
                Log 'Reading file owners...\n' ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%U | mapfile -t found_file_owners
288✔
574
                Log 'Reading file groups...\n' ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%G | mapfile -t found_file_groups
288✔
575
        fi
576

577
        LogLeave # Reading file attributes
72✔
578

579
        LogEnter 'Checking disk space...\n'
72✔
580
        local -i i
72✔
581
        local -i total_blocks=0
72✔
582
        local -i tmp_block_size tmp_blocks_free
72✔
583
        tmp_block_size=$(stat -f -c %S "$system_dir")
144✔
584
        tmp_blocks_free=$(stat -f -c %f "$system_dir")
144✔
585
        local tmp_fs_type
72✔
586
        tmp_fs_type=$(stat -f -c %T "$system_dir")
144✔
587
        if [[ "$tmp_fs_type" != "ramfs" ]]
72✔
588
        then
589
                for ((i=0; i<${#found_files[*]}; i++))
500✔
590
                do
591
                        local -i size="${found_file_sizes[$i]}"
178✔
592
                        local -i blocks=$(((size+tmp_block_size-1)/tmp_block_size)) # Count blocks
178✔
593
                        total_blocks+=$blocks
178✔
594
                        if (( total_blocks >= tmp_blocks_free ))
178✔
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
72✔
607

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

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

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

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

626
                if [[ -n "${found_file_edited[$file]+x}" ]]
145✔
627
                then
628
                        mkdir --parents "$(dirname "$system_dir"/files/"$file")"
288✔
629
                        if [[ "$type" == "symbolic link" ]]
144✔
630
                        then
631
                                ln -s -- "$(sudo readlink "$file")" "$system_dir"/files/"$file"
36✔
632
                        elif [[ "$type" == "regular file" || "$type" == "regular empty file" ]]
220✔
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" ]]
92✔
658
                        then
659
                                mkdir --parents "$system_dir"/files/"$file"
92✔
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
145✔
668
                        for prop in mode owner group
435✔
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 ]]
489✔
674
                                then
675
                                        continue
18✔
676
                                fi
677

678
                                local value
417✔
679
                                eval "value=\$$prop"
834✔
680

681
                                local default_value
417✔
682

683
                                if [[ $i -lt $stray_file_count ]]
417✔
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=
327✔
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
90✔
696
                                fi
697

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

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

706
        LogLeave # Processing found files
72✔
707

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

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

713
typeset -A file_property_kind_exists
6,557✔
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
426✔
719
        local prop=$2 # Name of the property (owner, group, or mode)
426✔
720
        local type="${3:-}" # Type of the file, as identified by `stat --format=%F`
426✔
721
        local default="${4:-}" # Default value, returned if we don't know the original file property.
426✔
722

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

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

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

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

744
                        if [[ "$type" == "symbolic link" ]]
110✔
745
                        then
746
                                FatalError 'Symbolic links do not have a mode\n' # Bug
×
747
                        elif [[ "$type" == "directory" ]]
110✔
748
                        then
749
                                printf 755
82✔
750
                        else
751
                                printf '%s' "$default_file_mode"
28✔
752
                        fi
753
                        ;;
754
                owner|group)
755
                        printf 'root'
226✔
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
210✔
763
        local varname="$2"  # Name of global associative array variable to read into
210✔
764

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

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

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

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

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

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

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

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

820
        LogLeave
136✔
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'
70✔
831

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

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

840
        typeset -ag system_only_files=()
140✔
841
        local file
70✔
842

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

850
        typeset -ag changed_files=()
140✔
851

852
        AconfNeedProgram diff diffutils n
70✔
853

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

861
                        if [[ "$output_type" != "$system_type" ]]
53✔
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" ]]
57✔
872
                        then
873
                                continue
9✔
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=()
140✔
884

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

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

897
        #
898
        # Modified file properties
899
        #
900

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

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

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

914
        AconfCompareFileProps
70✔
915

916
        LogLeave 'Done (%s only in system, %s changed, %s only in config).\n'        \
280✔
917
                         "$(Color G "${#system_only_file_props[@]}")"                                        \
280✔
918
                         "$(Color G "${#changed_file_props[@]}")"                                                \
280✔
919
                         "$(Color G "${#config_only_file_props[@]}")"
280✔
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'
69✔
929

930
        # Configuration
931

932
        AconfCompileOutput
69✔
933

934
        # System
935

936
        AconfCompileSystem
69✔
937

938
        # Vars
939

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

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

946
        AconfAnalyzeFiles
69✔
947

948
        LogLeave # Collecting data
69✔
949
}
950

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

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

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

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

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

978
        LogEnter 'Detecting AUR helper...\n'
×
979

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

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

996
base_devel_installed=n
6,556✔
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
×
1003
        local aur_rpc_url="https://aur.archlinux.org/rpc/?v=5"
×
1004
        local pkg_base=""
×
1005

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

1010
        if [[ -n "$response" ]]
×
1011
        then
1012
                resultcount=$(printf '%s' "$response" | sed -n 's/.*"resultcount":\([0-9]*\).*/\1/p')
×
1013
                if [[ "$resultcount" -gt 0 ]]
×
1014
                then
1015
                        pkg_base=$(printf '%s' "$response" | grep -o '"PackageBase":"[^"]*"' | head -1 | sed 's/"PackageBase":"\([^"]*\)"/\1/')
×
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" ]]
×
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"
×
1035
}
1036

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

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

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

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

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

1060
        rm -rf "$pkg_dir"
×
1061
        mkdir --parents "$pkg_dir"
×
1062

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

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

1075
                LogLeave
×
1076
                base_devel_installed=y
×
1077
        fi
1078

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

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

1087
                if [[ "$package" == auracle-git ]]
×
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'
×
1093

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

1098
                if [[ -z "$pkg_base" ]]
×
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")"
×
1106

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

1113
        AconfMakePkgDir "$package" "$asdeps" "$install" "$pkg_dir"
×
1114
}
1115

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

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

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

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

1143
                        if [[ ${#depends[@]} != 0 ]]
×
1144
                        then
1145
                                ( "$PACMAN" --deptest "${depends[@]}" || true ) | mapfile -t missing_depends
×
1146
                                if [[ ${#missing_depends[@]} != 0 ]]
×
1147
                                then
1148
                                        for dependency in "${missing_depends[@]}"
×
1149
                                        do
1150
                                                LogEnter '%s:\n' "$(Color M %q "$dependency")"
×
1151
                                                if "$PACMAN" --query --info "$dependency" > /dev/null 2>&1
×
1152
                                                then
1153
                                                        Log 'Already installed.\n' # Shouldn't happen, actually
×
1154
                                                elif "$PACMAN" --sync --info "$dependency" > /dev/null 2>&1
×
1155
                                                then
1156
                                                        Log 'Installing from repositories...\n'
×
1157
                                                        AconfInstallNative --asdeps "$dependency"
×
1158
                                                        Log 'Installed.\n'
×
1159
                                                else
1160
                                                        local installed=false
×
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
×
1166
                                                        AconfNeedProgram unbuffer expect n
×
1167
                                                        local providers
×
1168
                                                        providers=$(unbuffer pacsift --sync --exact --satisfies="$dependency")
1169
                                                        if [[ -n "$providers" ]]
×
1170
                                                        then
1171
                                                                Log 'Installing provider package from repositories...\n'
×
1172
                                                                AconfInstallNative --asdeps "$dependency"
×
1173
                                                                Log 'Installed.\n'
×
1174
                                                                installed=true
×
1175
                                                        fi
1176

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

1185
                                                LogLeave ''
×
1186
                                        done
1187
                                fi
1188
                        fi
1189

1190
                        LogLeave
×
1191

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

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

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

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

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

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

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

1245
                                        LogLeave
×
1246
                                done
1247

1248
                                LogLeave
×
1249
                        fi
1250
                fi
1251
        done
1252

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

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

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

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

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

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

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

1317
                        if $install
×
1318
                        then
1319
                                args+=(--install)
1320
                        fi
1321

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

1327
        LogLeave
×
1328
}
1329

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

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

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

1358
        local target_packages=("$@")
1359

1360
        DetectAurHelper
×
1361

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

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

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

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

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

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

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

1462
                        local filemask
7✔
1463
                        if $precise
7✔
1464
                        then
1465
                                filemask=$filemask_precise
7✔
1466
                        else
1467
                                filemask=$filemask_any
×
1468
                        fi
1469

1470
                        local dirs=()
14✔
1471
                        if $foreign
7✔
1472
                        then
1473
                                DetectAurHelper
×
1474
                                local -A tried_helper=()
1475

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

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

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

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

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

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

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

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

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

1635
        local package_file
7✔
1636
        package_file="$(AconfNeedPackageFile "$package")"
14✔
1637

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

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

1651
        local package_file
×
1652
        package_file="$(AconfNeedPackageFile "$package")"
×
1653

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

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

1663
        AconfReplace "$tmp_file" "$file"
×
1664
        sudo rm -rf "$tmp_base"
×
1665
}
1666

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

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

1693
####################################################################################################
1694

1695
prompt_mode=normal # never / normal / paranoid
6,557✔
1696

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

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

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

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

1741
####################################################################################################
1742

1743
log_indent=:
6,557✔
1744

1745
function Log() {
1746
        if [[ "$#" != 0 && -n "$1" ]]
12,284✔
1747
        then
1748
                local fmt="$1"
5,914✔
1749
                shift
5,913✔
1750

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

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

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

1766
function LogEnter() {
1767
        Log "$@"
2,395✔
1768
        log_indent=$log_indent:
2,382✔
1769
}
1770

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

1779
        log_indent=${log_indent::-1}
2,391✔
1780
}
1781

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

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

1795
function Color() {
1796
        local var="ANSI_color_$1"
3,014✔
1797
        printf -- "%s" "${!var}"
3,015✔
1798
        shift
3,015✔
1799
        # shellcheck disable=2059
1800
        printf -- "$@"
3,015✔
1801
        printf -- "%s" "${ANSI_color_W}"
3,014✔
1802
}
1803

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

1818
####################################################################################################
1819

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

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

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

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

1840
        LogLeave ''
50✔
1841

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

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

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

1870
####################################################################################################
1871

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

1877
        size="$(eval "echo \${#${name}""[*]}")"
2,748✔
1878
        if [[ $size != 0 ]]
916✔
1879
        then
1880
                eval "echo \"\${${name}[*]}\""
272✔
1881
        fi
1882
}
1883

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

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

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

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

1918
if [[ $EUID == 0 ]]
6,557✔
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 "$@"
×
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"
×
1939
        else
1940
                sudo cat "$1"
1✔
1941
        fi
1942
}
1943

1944
if ! ( empty_array=() ; : "${empty_array[@]}" )
13,114✔
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
6,557✔
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