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

CyberShadow / aconfmgr / 684

04 May 2026 02:02AM UTC coverage: 91.145% (-2.5%) from 93.67%
684

push

github

CyberShadow
README: Clarify config file naming convention

Document that configuration files should begin with two digits, matching
the examples given (10-base.sh, 20-drivers.sh, etc.). This makes the
naming requirement explicit rather than implied by the examples, and
avoids pitfalls such as #241.

4601 of 5048 relevant lines covered (91.15%)

396.32 hits per line

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

93.92
/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,607✔
10

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

25
default_file_mode=644
6,607✔
26

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

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

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

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

46
# Defaults
47

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

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

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

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

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

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

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

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

133
        # Configuration
134

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

137
        typeset -ag ignore_packages=()
244✔
138
        typeset -ag ignore_foreign_packages=()
244✔
139
        typeset -Ag used_files
122✔
140

141
        local found=n
122✔
142
        local file
122✔
143
        local saw_unsorted=n
122✔
144
        local files_after_unsorted=()
244✔
145
        for file in "$config_dir"/*.sh
160✔
146
        do
147
                if [[ -e "$file" ]]
160✔
148
                then
149
                        if [[ $saw_unsorted == y ]]
142✔
150
                        then
151
                                files_after_unsorted+=("$file")
1✔
152
                        fi
153
                        if [[ "$(basename -- "$file")" == 99-unsorted.sh ]]
284✔
154
                        then
155
                                saw_unsorted=y
6✔
156
                        fi
157
                        LogEnter 'Sourcing %s...\n' "$(Color C "%q" "$file")"
284✔
158
                        # shellcheck source=/dev/null
159
                        source "$file"
142✔
160
                        found=y
142✔
161
                        LogLeave ''
142✔
162
                fi
163
        done
164

165
        local f
122✔
166
        for f in "${files_after_unsorted[@]}"
1✔
167
        do
168
                ConfigWarning \
×
169
                        '%s is sourced after %s. '\
×
170
'aconfmgr save appends entries to %s, which is intended to sort last '\
×
171
'but does not ('\''9'\'' sorts before letters in ASCII byte ordering). '\
×
172
'When the same path appears in both files, the later-sourced file may fail with '\
×
173
'errors like "File exists". Rename %s so it sorts before %s.\n' \
6✔
174
                        "$(Color C "%q" "$f")" \
6✔
175
                        "$(Color C "99-unsorted.sh")" \
6✔
176
                        "$(Color C "99-unsorted.sh")" \
6✔
177
                        "$(Color C "%q" "$f")" \
6✔
178
                        "$(Color C "99-unsorted.sh")"
6✔
179
        done
180

181
        if $lint_config
122✔
182
        then
183
                # Check for unused files (files not referenced by the CopyFile
184
                # helper).
185
                # Only do this in the "check" action, as unused files do not
186
                # necessarily indicate a bug in the configuration - they may
187
                # simply be used under certain conditions.
188
                if [[ -d "$config_dir"/files ]]
6✔
189
                then
190
                        local line
3✔
191
                        find "$config_dir"/files -type f -print0 | \
3✔
192
                                while read -r -d $'\0' line
6✔
193
                                do
194
                                        local key=${line#"$config_dir"/files}
3✔
195
                                        if [[ -z "${used_files[$key]+x}" ]]
3✔
196
                                        then
197
                                                ConfigWarning 'Unused file: %s\n' \
4✔
198
                                                                          "$(Color C "%q" "$line")"
4✔
199
                                        fi
200
                                done
201
                fi
202
        fi
203

204
        if [[ $found == y ]]
122✔
205
        then
206
                LogLeaveDirStats "$output_dir"
104✔
207
        else
208
                LogLeave 'Done (configuration not found).\n'
18✔
209
        fi
210
}
211

212
skip_inspection=n
6,603✔
213
skip_checksums=n
6,603✔
214

215
# Given a list of paths to ignore (which can contain shell patterns),
216
# creates the list of arguments to give to 'find' to ignore them.
217
# The result is stored in the variable given by name as the first argument.
218
function AconfCreateFindIgnoreArgs() {
219
        # A correct and simple implementation of this function would just give
220
        # each argument as a parameter to find's -wholename, e.g. result in
221
        # (-wholename path1 -o -wholename path2 -o ... -o -wholename pathn)
222
        # However, this is painfully slow if the number of ignore patterns is large
223
        # Instead, this function (ab)uses the fact that if the number of patterns
224
        # given to GNU find is big, then as an implementation detail, using regular
225
        # expressions (-regex option and similar) scales much better than using
226
        # shell patterns (-wholename option and similar)
227
        local ignore_args_varname=$1
115✔
228
        local -n ignore_args_var=$ignore_args_varname
115✔
229
        shift
115✔
230
        local ignore_paths=("$@")
230✔
231

232
        # Divide the ignore paths as simple (contain obviously literal ASCII
233
        # characters or an asterisk wildcard) or complex (such as those using
234
        # character ranges, question mark wildcards, international characters, etc.)
235
        # Simple ignore paths are later going to be converted to regex,
236
        # complex ignore paths are passed literally to find's -wholename
237
        local simple_ignore_paths=()
230✔
238

239
        local ignore_path
115✔
240
        for ignore_path in "${ignore_paths[@]}"
1,708✔
241
        do
242
                # In this regular expression, we don't use ranges like "a-z", since this
243
                # can match non-ASCII characters. Also, the hyphen is at the end of the
244
                # regular expression to avoid it being interpreted as a range
245
                if [[ "$ignore_path" =~ [^abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_/\ .*-] ]]
1,708✔
246
                then
247
                        ignore_args_var+=(-wholename "$ignore_path" -o)
10✔
248
                else
249
                        simple_ignore_paths+=("${ignore_path}")
1,698✔
250
                fi
251
        done
252

253
        if [ ${#simple_ignore_paths[@]} -ne 0 ]
115✔
254
        then
255
                # Converting the simple ignore paths to regular expressions just needs to
256
                # handle the asterisk wildcard, and escape a few characters
257
                local ignore_regexps
115✔
258
                echo -n "${simple_ignore_paths[*]}" | \
115✔
259
                        sed 's|[^*abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_/ ]|[&]|g; s|\*|.*|g' | \
115✔
260
                        mapfile -t ignore_regexps
115✔
261

262
                ignore_args_var+=(-regex "$( IFS='|' ; echo "${ignore_regexps[*]}" )" -o)
345✔
263
        fi
264

265
        ignore_args_var+=(-false)
115✔
266
}
267

268
# Collect system state into $system_dir
269
function AconfCompileSystem() {
270
        LogEnter 'Inspecting system state...\n'
117✔
271

272
        if [[ $skip_inspection == y ]]
117✔
273
        then
274
                LogLeave 'Skipped.\n'
2✔
275
                return
2✔
276
        fi
277

278
        # shellcheck disable=SC2174
279
        mkdir --mode=700 --parents "$tmp_dir"
115✔
280

281
        rm -rf "$system_dir"
115✔
282
        mkdir --parents "$system_dir"
115✔
283
        mkdir "$system_dir"/files
115✔
284
        touch "$system_dir"/file-props.txt
115✔
285
        touch "$system_dir"/orig-file-props.txt
115✔
286

287
        ### Packages
288

289
        LogEnter 'Querying package list...\n'
115✔
290
        ( "$PACMAN" --query --quiet --explicit --native  || true ) | sort | ( grep -vFxf <(PrintArray ignore_packages        ) || true ) > "$system_dir"/packages.txt
554✔
291
        ( "$PACMAN" --query --quiet --explicit --foreign || true ) | sort | ( grep -vFxf <(PrintArray ignore_foreign_packages) || true ) > "$system_dir"/foreign-packages.txt
678✔
292
        LogLeave
115✔
293

294
        ### Files
295

296
        local -a found_files
115✔
297
        found_files=()
115✔
298

299
        # whether the contents is different from the original
300
        local -A found_file_edited
115✔
301
        found_file_edited=()
115✔
302

303
        # Stray files
304

305
        local -a ignore_args
115✔
306
        AconfCreateFindIgnoreArgs ignore_args "${ignore_paths[@]}"
115✔
307

308
        LogEnter 'Enumerating owned files...\n'
115✔
309
        mkdir --parents "$tmp_dir"
115✔
310
        ( "$PACMAN" --query --list --quiet || true ) | sed 's#\/$##' | sort --unique > "$tmp_dir"/owned-files
345✔
311
        LogLeave
115✔
312

313
        LogEnter 'Searching for stray files...\n'
115✔
314

315
        local line
115✔
316
        local -Ag ignored_dirs
115✔
317

318
        AconfNeedProgram gawk gawk n
115✔
319

320
        # Progress display - only show file names once per second
321
        local progress_fd
115✔
322
        exec {progress_fd}> \
158✔
323
                 >( gawk '
158✔
324
BEGIN {
×
325
    RS = "\0";
×
326
    t = systime();
×
327
};
×
328
{
329
    u = systime();
×
330
    if (t != u) {
×
331
        t = u;
×
332
        printf "%s\0", $0;
×
333
        system(""); # https://unix.stackexchange.com/a/83853/4830
×
334
        }
187✔
335
}' | \
×
336
                                while read -r -d $'\0' line
×
337
                                do
338
                                        local path=${line:1}
×
339
                                        path=${path%/*} # Never show files, only directories
×
340
                                        while [[ ${#path} -gt 40 ]]
×
341
                                        do
342
                                                path=${path%/*}
×
343
                                        done
344
                                        Log 'Scanning %s...\r' "$(Color M "%q" "$path")"
×
345
                                done
346
                  )
347

348
        local stray_file_count=0
115✔
349
        (
350
                # NB: Regular expressions can be generated by AconfCreateFindIgnoreArgs
351
                #     The posix-extended regex type is used since it's easier to work
352
                #     with (e.g. no need to escape '|' alternations, parenthesis, etc.)
353
                sudo find /                                                                        \
115✔
354
                         -regextype posix-extended                                \
×
355
                         -not                                                                        \
×
356
                         \(                                                                                \
×
357
                                 \(                                                                        \
×
358
                                        "${ignore_args[@]}"                                \
×
359
                                \)                                                                        \
×
360
                                -printf 'I' -print0 -prune                        \
×
361
                        \)                                                                                \
×
362
                        -printf 'O' -print0                                                \
×
363
                        | tee /dev/fd/$progress_fd                                \
115✔
364
                        | ( grep                                                                \
86✔
365
                                        --null --null-data                                \
144✔
366
                                        --invert-match                                        \
×
367
                                        --fixed-strings                                        \
×
368
                                        --line-regexp                                        \
×
369
                                        --file                                                        \
×
370
                                        <( < "$tmp_dir"/owned-files                \
×
371
                                                 sed -e 's#^#O#'                        \
×
372
                                         )                                                                \
×
373
                                        || true )                                                \
×
374
        ) |                                                                                                \
×
375
                while read -r -d $'\0' line
2,320✔
376
                do
377
                        local file action
2,309✔
378
                        file=${line:1}
2,302✔
379
                        action=${line:0:1}
2,294✔
380

381
                        case "$action" in
2,213✔
382
                                O) # Stray file
383
                                        #echo "ignore_paths+='$file' # "
384
                                        if ((verbose))
198✔
385
                                        then
386
                                                Log '%s\r' "$(Color C "%q" "$file")"
×
387
                                        fi
388

389
                                        found_files+=("$file")
199✔
390
                                        found_file_edited[$file]=y
199✔
391
                                        stray_file_count=$((stray_file_count+1))
198✔
392

393
                                        if [[ $stray_file_count -eq $warn_file_count_threshold ]]
198✔
394
                                        then
395
                                                LogEnter '%s: reached %s stray files while in directory %s.\n' \
×
396
                                                        "$(Color Y "Warning")" \
×
397
                                                        "$(Color G "$stray_file_count")" \
×
398
                                                        "$(Color C "%q" "$(dirname "$file")")"
×
399
                                                LogLeave 'Perhaps add %s (or a parent directory) to configuration to ignore it.\n' \
×
400
                                                                 "$(Color Y "IgnorePath %q" "$(dirname "$file")"/'*')"
×
401
                                                warn_file_count_threshold=$((warn_file_count_threshold * 10))
×
402
                                        fi
403
                                        ;;
404
                                I) # Ignored
405

406
                                        # For convenience, we want to also ignore
407
                                        # directories which contain only ignored files.
408
                                        #
409
                                        # This is so that a rule such as:
410
                                        #
411
                                        # IgnorePath '/foo/bar/baz/*.log'
412
                                        #
413
                                        # does not cause `aconfmgr save` to still emit lines like
414
                                        #
415
                                        # CreateDir /foo/bar
416
                                        # CreateDir /foo/bar/baz
417
                                        #
418
                                        # However, we can't simply exclude parent dirs of
419
                                        # excluded files from the file list, as then they
420
                                        # will show up as missing in the diff against the
421
                                        # compiled configuration. So, later we remove
422
                                        # parent directories of any found un-ignored
423
                                        # files.
424

425
                                        local path="$file"
2,015✔
426
                                        while [[ -n "$path" ]]
5,822✔
427
                                        do
428
                                                ignored_dirs[$path]=y
3,738✔
429
                                                path=${path%/*}
3,720✔
430
                                        done
431
                                        ;;
432
                        esac
433
                done
434

435
        LogLeave 'Done (%s stray files).\n' "$(Color G %s $stray_file_count)"
204✔
436

437
        exec {progress_fd}<&-
111✔
438

439
        LogEnter 'Cleaning up ignored files'\'' directories...\n'
111✔
440

441
        local file
101✔
442
        for file in "${found_files[@]}"
199✔
443
        do
444
                if [[ -z "${ignored_dirs[$file]+x}" ]]
176✔
445
                then
446
                        local path="$file"
119✔
447
                        while [[ -n "$path" ]]
247✔
448
                        do
449
                                unset "ignored_dirs[\$path]"
126✔
450
                                path=${path%/*}
127✔
451
                        done
452
                fi
453
        done
454

455
        LogLeave
78✔
456

457
        # Modified files
458

459
        LogEnter 'Searching for modified files...\n'
77✔
460

461
        AconfNeedProgram paccheck pacutils n
79✔
462
        AconfNeedProgram unbuffer expect n
81✔
463
        local modified_file_count=0
83✔
464
        local -A saw_file
80✔
465

466
        # Tentative tracking of original file properties.
467
        # The canonical version is read from orig-file-props.txt in AconfAnalyzeFiles
468
        unset orig_file_props ; typeset -Ag orig_file_props
159✔
469

470
        : > "$tmp_dir"/file-owners
81✔
471

472
        local paccheck_opts=(unbuffer paccheck --files --file-properties --backup --noupgrade)
178✔
473
        if [[ $skip_checksums == n ]]
89✔
474
        then
475
                # Use SHA256 for pacman 7.0+ (libalpm v15+) which uses SHA256 in mtree
476
                # Fall back to MD5 for older versions
477
                if paccheck --help 2>&1 | grep -q -- '--sha256sum'
191✔
478
                then
479
                        paccheck_opts+=(--sha256sum)
42✔
480
                else
481
                        paccheck_opts+=(--md5sum)
69✔
482
                fi
483
        fi
484

485
        sudo sh -c "LC_ALL=C stdbuf -o0 $(printf ' %q' "${paccheck_opts[@]}") 2>&1 || true" | \
230✔
486
                while read -r line
31,935✔
487
                do
488
                        if [[ $line =~ ^(.*):\ \'(.*)\'\ (type|size|modification\ time|md5sum|sha256sum|UID|GID|permission|symlink\ target)\ mismatch\ \(expected\ (.*)\)$ ]]
31,803✔
489
                        then
490
                                local package="${BASH_REMATCH[1]}"
1,157✔
491
                                local file="${BASH_REMATCH[2]}"
1,153✔
492
                                local kind="${BASH_REMATCH[3]}"
1,155✔
493
                                local value="${BASH_REMATCH[4]}"
1,157✔
494

495
                                local ignored=n
1,156✔
496
                                local ignore_path
1,155✔
497
                                for ignore_path in "${ignore_paths[@]}"
19,833✔
498
                                do
499
                                        # shellcheck disable=SC2053
500
                                        if [[ "$file" == $ignore_path ]]
19,777✔
501
                                        then
502
                                                ignored=y
827✔
503
                                                break
820✔
504
                                        fi
505
                                done
506

507
                                if [[ $ignored == n ]]
913✔
508
                                then
509
                                        if [[ -z "${saw_file[$file]+x}" ]]
95✔
510
                                        then
511
                                                saw_file[$file]=y
36✔
512
                                                Log '%s: %s\n' "$(Color M "%q" "$package")" "$(Color C "%q" "$file")"
108✔
513
                                                found_files+=("$file")
36✔
514
                                                modified_file_count=$((modified_file_count+1))
36✔
515
                                        fi
516

517
                                        local prop
95✔
518
                                        case "$kind" in
96✔
519
                                                UID)
520
                                                        prop=owner
15✔
521
                                                        value=${value#*/}
15✔
522
                                                        ;;
523
                                                GID)
524
                                                        prop=group
14✔
525
                                                        value=${value#*/}
14✔
526
                                                        ;;
527
                                                permission)
528
                                                        prop=mode
18✔
529
                                                        ;;
530
                                                type|size|modification\ time|md5sum|sha256sum|symlink\ target)
531
                                                        prop=
49✔
532
                                                        found_file_edited[$file]=y
49✔
533
                                                        ;;
534
                                                *)
535
                                                        prop=
×
536
                                                        ;;
537
                                        esac
538

539
                                        if [[ -n "$prop" ]]
96✔
540
                                        then
541
                                                local key="$file:$prop"
47✔
542
                                                orig_file_props[$key]=$value
47✔
543

544
                                                printf '%s\t%s\t%q\n' "$prop" "$value" "$file" >> "$system_dir"/orig-file-props.txt
47✔
545
                                        fi
546
                                fi
547
                                printf '%s\0%s\0' "$file" "$package" >> "$tmp_dir"/file-owners
916✔
548
                        elif [[ $line =~ ^(.*):\ \'(.*)\'\ missing\ file$ ]]
30,894✔
549
                        then
550
                                local package="${BASH_REMATCH[1]}"
7✔
551
                                local file="${BASH_REMATCH[2]}"
7✔
552

553
                                local ignored=n
7✔
554
                                local ignore_path
7✔
555
                                for ignore_path in "${ignore_paths[@]}"
89✔
556
                                do
557
                                        # shellcheck disable=SC2053
558
                                        if [[ "$file" == $ignore_path ]]
88✔
559
                                        then
560
                                                ignored=y
1✔
561
                                                break
1✔
562
                                        fi
563
                                done
564

565
                                if [[ $ignored == y ]]
5✔
566
                                then
567
                                        continue
1✔
568
                                fi
569

570
                                Log '%s (missing)...\r' "$(Color M "%q" "$package")"
12✔
571
                                printf '%s\t%s\t%q\n' "deleted" "y" "$file" >> "$system_dir"/file-props.txt
7✔
572
                                printf '%s\0%s\0' "$file" "$package" >> "$tmp_dir"/file-owners
7✔
573
                        elif [[ $line =~ ^warning:\ (.*):\ \'(.*)\'\ read\ error\ \(No\ such\ file\ or\ directory\)$ ]]
30,810✔
574
                        then
575
                                local package="${BASH_REMATCH[1]}"
205✔
576
                                local file="${BASH_REMATCH[2]}"
205✔
577
                                # Ignore
578
                        elif [[ $line =~ ^(.*):\ all\ files\ match\ (database|mtree|mtree\ md5sums|mtree\ sha256sums)$ ]]
30,607✔
579
                        then
580
                                local package="${BASH_REMATCH[1]}"
30,740✔
581
                                Log '%s...\r' "$(Color M "%q" "$package")"
61,802✔
582
                                #echo "Now at ${BASH_REMATCH[1]}"
583
                        else
584
                                Log 'Unknown paccheck output line: %s\n' "$(Color Y "%q" "$line")"
×
585
                        fi
586
                done
587
        LogLeave 'Done (%s modified files).\n' "$(Color G %s $modified_file_count)"
230✔
588

589
        LogEnter 'Reading file attributes...\n'
115✔
590

591
        typeset -a found_file_types found_file_sizes found_file_modes found_file_owners found_file_groups
115✔
592
        if [[ ${#found_files[*]} == 0 ]]
115✔
593
        then
594
                Log 'No files found, skipping.\n'
×
595
        else
596
                Log 'Reading file types...\n'  ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%F | mapfile -t  found_file_types
457✔
597
                Log 'Reading file sizes...\n'  ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%s | mapfile -t  found_file_sizes
460✔
598
                Log 'Reading file modes...\n'  ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%a | mapfile -t  found_file_modes
460✔
599
                Log 'Reading file owners...\n' ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%U | mapfile -t found_file_owners
460✔
600
                Log 'Reading file groups...\n' ; Print0Array found_files | sudo env LC_ALL=C xargs -0 stat --format=%G | mapfile -t found_file_groups
460✔
601
        fi
602

603
        LogLeave # Reading file attributes
115✔
604

605
        LogEnter 'Checking disk space...\n'
115✔
606
        local -i i
115✔
607
        local -i total_blocks=0
115✔
608
        local -i tmp_block_size tmp_blocks_free
115✔
609
        tmp_block_size=$(stat -f -c %S "$system_dir")
230✔
610
        tmp_blocks_free=$(stat -f -c %f "$system_dir")
230✔
611
        local tmp_fs_type
115✔
612
        tmp_fs_type=$(stat -f -c %T "$system_dir")
230✔
613
        if [[ "$tmp_fs_type" != "ramfs" ]]
115✔
614
        then
615
                for ((i=0; i<${#found_files[*]}; i++))
968✔
616
                do
617
                        local -i size="${found_file_sizes[$i]}"
369✔
618
                        local -i blocks=$(((size+tmp_block_size-1)/tmp_block_size)) # Count blocks
369✔
619
                        total_blocks+=$blocks
369✔
620
                        if (( total_blocks >= tmp_blocks_free ))
369✔
621
                        then
622
                                local file="${found_files[$i]}"
×
623
                                Log 'Copying file %s (%s bytes / %s blocks) to temporary storage would exhaust free space on %s (%s bytes / %s blocks).\n' \
×
624
                                        "$(Color C "%q" "$file")" "$(Color G "$size")" "$(Color G "$blocks")" \
×
625
                                        "$(Color C "%q" "$system_dir")" "$(Color G "$((tmp_blocks_free * tmp_block_size))")" "$(Color G "$tmp_blocks_free")"
×
626
                                Log 'Perhaps add %s (or a parent directory) to configuration to ignore it, or run with %s pointing at another location.\n' \
×
627
                                        "$(Color Y "IgnorePath %q" "$(dirname "$file")"/'*')" "$(Color Y "TMPDIR")"
×
628
                                FatalError 'Refusing to proceed.\n'
×
629
                        fi
630
                done
631
        fi
632
        LogLeave
115✔
633

634
        LogEnter 'Processing found files...\n'
115✔
635

636
        for ((i=0; i<${#found_files[*]}; i++))
968✔
637
        do
638
                Log '%s/%s...\r' "$(Color G "$i")" "$(Color G "${#found_files[*]}")"
1,106✔
639

640
                local  file="${found_files[$i]}"
368✔
641
                local  type="${found_file_types[$i]}"
368✔
642
                local  size="${found_file_sizes[$i]}"
368✔
643
                local  mode="${found_file_modes[$i]}"
368✔
644
                local owner="${found_file_owners[$i]}"
368✔
645
                local group="${found_file_groups[$i]}"
368✔
646

647
                if [[ "${ignored_dirs[$file]-n}" == y ]]
368✔
648
                then
649
                        continue
162✔
650
                fi
651

652
                if [[ -n "${found_file_edited[$file]+x}" ]]
206✔
653
                then
654
                        mkdir --parents "$(dirname "$system_dir"/files/"$file")"
411✔
655
                        if [[ "$type" == "symbolic link" ]]
206✔
656
                        then
657
                                ln -s -- "$(sudo readlink "$file")" "$system_dir"/files/"$file"
38✔
658
                        elif [[ "$type" == "regular file" || "$type" == "regular empty file" ]]
329✔
659
                        then
660
                                if [[ $size -gt $warn_size_threshold ]]
47✔
661
                                then
662
                                        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")"
×
663
                                fi
664

665
                                local filter_pattern filter_func
47✔
666
                                unset filter_func
47✔
667
                                for filter_pattern in "${!file_content_filters[@]}"
6✔
668
                                do
669
                                        # shellcheck disable=SC2053
670
                                        if [[ "$file" == $filter_pattern ]]
6✔
671
                                        then
672
                                                filter_func=${file_content_filters[$filter_pattern]}
6✔
673
                                        fi
674
                                done
675

676
                                if [[ -v filter_func ]]
47✔
677
                                then
678
                                        sudo cat "$file" | "$filter_func" "$file" > "$system_dir"/files/"$file"
12✔
679
                                else
680
                                        # shellcheck disable=SC2024
681
                                        sudo cat "$file" > "$system_dir"/files/"$file"
41✔
682
                                fi
683
                        elif [[ "$type" == "directory" ]]
140✔
684
                        then
685
                                mkdir --parents "$system_dir"/files/"$file"
140✔
686
                        else
687
                                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")"
×
688
                                continue
×
689
                        fi
690
                fi
691

692
                {
693
                        local prop
207✔
694
                        for prop in mode owner group
621✔
695
                        do
696
                                # Ignore mode "changes" in symbolic links
697
                                # If a file's type changes, a change in mode can be reported too.
698
                                # But, symbolic links cannot have a mode, so ignore this change.
699
                                if [[ "$type" == "symbolic link" && "$prop" == mode ]]
678✔
700
                                then
701
                                        continue
19✔
702
                                fi
703

704
                                local value
602✔
705
                                eval "value=\$$prop"
1,203✔
706

707
                                local default_value
601✔
708

709
                                if [[ $i -lt $stray_file_count ]]
601✔
710
                                then
711
                                        # For stray files, the default owner/group is root/root,
712
                                        # and the default mode depends on the type.
713
                                        # Let AconfDefaultFileProp get the correct default value for us.
714

715
                                        default_value=
509✔
716
                                else
717
                                        # For owned files, we assume that the defaults are the
718
                                        # files' current properties, unless paccheck said
719
                                        # otherwise.
720

721
                                        default_value=$value
92✔
722
                                fi
723

724
                                local orig_value
601✔
725
                                orig_value=$(AconfDefaultFileProp "$file" "$prop" "$type" "$default_value")
1,204✔
726

727
                                [[ "$value" == "$orig_value" ]] || printf '%s\t%s\t%q\n' "$prop" "$value" "$file"
664✔
728
                        done
729
                } >> "$system_dir"/file-props.txt
×
730
        done
731

732
        LogLeave # Processing found files
115✔
733

734
        LogLeaveDirStats "$system_dir" # Inspecting system state
115✔
735
}
736

737
####################################################################################################
738

739
typeset -A file_property_kind_exists
6,607✔
740

741
# Print to stdout the original/default value of the given file property.
742
# Uses orig_file_props entry if present.
743
function AconfDefaultFileProp() {
744
        local file=$1 # Absolute path to the file
620✔
745
        local prop=$2 # Name of the property (owner, group, or mode)
620✔
746
        local type="${3:-}" # Type of the file, as identified by `stat --format=%F`
620✔
747
        local default="${4:-}" # Default value, returned if we don't know the original file property.
620✔
748

749
        local key="$file:$prop"
620✔
750

751
        if [[ -n "${orig_file_props[$key]+x}" ]]
620✔
752
        then
753
                printf '%s' "${orig_file_props[$key]}"
40✔
754
                return
40✔
755
        fi
756

757
        if [[ -n "$default" ]]
580✔
758
        then
759
                printf '%s' "$default"
53✔
760
                return
53✔
761
        fi
762

763
        case "$prop" in
527✔
764
                mode)
765
                        if [[ -z "$type" ]]
173✔
766
                        then
767
                                type=$(sudo env LC_ALL=C stat --format=%F "$file")
12✔
768
                        fi
769

770
                        if [[ "$type" == "symbolic link" ]]
173✔
771
                        then
772
                                FatalError 'Symbolic links do not have a mode\n' # Bug
×
773
                        elif [[ "$type" == "directory" ]]
173✔
774
                        then
775
                                printf 755
130✔
776
                        else
777
                                printf '%s' "$default_file_mode"
43✔
778
                        fi
779
                        ;;
780
                owner|group)
781
                        printf 'root'
354✔
782
                        ;;
783
        esac
784
}
785

786
# Read a file-props.txt file into an associative array.
787
function AconfReadFileProps() {
788
        local filename="$1" # Path to file-props.txt to be read
333✔
789
        local varname="$2"  # Name of global associative array variable to read into
333✔
790

791
        local line
333✔
792
        while read -r line
842✔
793
        do
794
                if [[ $line =~ ^(.*)\        (.*)\        (.*)$ ]]
509✔
795
                then
796
                        local kind="${BASH_REMATCH[1]}"
509✔
797
                        local value="${BASH_REMATCH[2]}"
509✔
798
                        local file="${BASH_REMATCH[3]}"
509✔
799
                        file="$(eval "printf %s $file")" # Unescape
1,527✔
800

801
                        if [[ -z "$value" ]]
509✔
802
                        then
803
                                unset "${varname}[\$file:\$kind]"
154✔
804
                        else
805
                                eval "${varname}[\$file:\$kind]=\"\$value\""
710✔
806
                        fi
807

808
                        file_property_kind_exists[$kind]=y
509✔
809
                fi
810
        done < "$filename"
×
811
}
812

813
# Compare file properties.
814
function AconfCompareFileProps() {
815
        LogEnter 'Comparing file properties...\n'
220✔
816

817
        typeset -ag system_only_file_props=()
438✔
818
        typeset -ag changed_file_props=()
438✔
819
        typeset -ag config_only_file_props=()
438✔
820

821
        local key
219✔
822
        for key in "${!system_file_props[@]}"
134✔
823
        do
824
                if [[ -z "${output_file_props[$key]+x}" ]]
133✔
825
                then
826
                        system_only_file_props+=("$key")
35✔
827
                fi
828
        done
829

830
        for key in "${!system_file_props[@]}"
99✔
831
        do
832
                if [[ -n "${output_file_props[$key]+x}" && "${system_file_props[$key]}" != "${output_file_props[$key]}" ]]
165✔
833
                then
834
                        changed_file_props+=("$key")
19✔
835
                fi
836
        done
837

838
        for key in "${!output_file_props[@]}"
108✔
839
        do
840
                if [[ -z "${system_file_props[$key]+x}" ]]
108✔
841
                then
842
                        config_only_file_props+=("$key")
57✔
843
                fi
844
        done
845

846
        LogLeave
216✔
847
}
848

849
# Compare file information in $output_dir and $system_dir.
850
function AconfAnalyzeFiles() {
851

852
        #
853
        # Stray/modified files - diff
854
        #
855

856
        LogEnter 'Examining files...\n'
111✔
857

858
        LogEnter 'Loading data...\n'
111✔
859
        mkdir --parents "$tmp_dir"
111✔
860
        ( cd "$output_dir"/files && find . -mindepth 1 -print0 ) | cut --zero-terminated -c 2- | sort --zero-terminated > "$tmp_dir"/output-files
444✔
861
        ( cd "$system_dir"/files && find . -mindepth 1 -print0 ) | cut --zero-terminated -c 2- | sort --zero-terminated > "$tmp_dir"/system-files
444✔
862
        LogLeave
111✔
863

864
        Log 'Comparing file data...\n'
111✔
865

866
        typeset -ag system_only_files=()
222✔
867
        local file
111✔
868

869
        ( comm -13 --zero-terminated "$tmp_dir"/output-files "$tmp_dir"/system-files ) | \
111✔
870
                while read -r -d $'\0' file
159✔
871
                do
872
                        Log 'Only in system: %s\n' "$(Color C "%q" "$file")"
96✔
873
                        system_only_files+=("$file")
48✔
874
                done
875

876
        typeset -ag changed_files=()
222✔
877

878
        AconfNeedProgram diff diffutils n
111✔
879

880
        ( comm -12 --zero-terminated "$tmp_dir"/output-files "$tmp_dir"/system-files ) | \
111✔
881
                while read -r -d $'\0' file
169✔
882
                do
883
                        local output_type system_type
58✔
884
                        output_type=$(LC_ALL=C stat --format=%F "$output_dir"/files/"$file")
174✔
885
                        system_type=$(LC_ALL=C stat --format=%F "$system_dir"/files/"$file")
174✔
886

887
                        if [[ "$output_type" != "$system_type" ]]
58✔
888
                        then
889
                                Log 'Changed type (%s / %s): %s\n' \
80✔
890
                                        "$(Color Y "%q" "$output_type")" \
80✔
891
                                        "$(Color Y "%q" "$system_type")" \
80✔
892
                                        "$(Color C "%q" "$file")"
80✔
893
                                changed_files+=("$file")
20✔
894
                                continue
20✔
895
                        fi
896

897
                        if [[ "$output_type" == "directory" || "$system_type" == "directory" ]]
67✔
898
                        then
899
                                continue
9✔
900
                        fi
901

902
                        if ! diff --no-dereference --brief "$output_dir"/files/"$file" "$system_dir"/files/"$file" > /dev/null
29✔
903
                        then
904
                                Log 'Changed: %s\n' "$(Color C "%q" "$file")"
20✔
905
                                changed_files+=("$file")
10✔
906
                        fi
907
                done
908

909
        typeset -ag config_only_files=()
222✔
910

911
        ( comm -23 --zero-terminated "$tmp_dir"/output-files "$tmp_dir"/system-files ) | \
111✔
912
                while read -r -d $'\0' file
151✔
913
                do
914
                        Log 'Only in config: %s\n' "$(Color C "%q" "$file")"
80✔
915
                        config_only_files+=("$file")
40✔
916
                done
917

918
        LogLeave 'Done (%s only in system, %s changed, %s only in config).\n'        \
444✔
919
                         "$(Color G "${#system_only_files[@]}")"                                                \
444✔
920
                         "$(Color G "${#changed_files[@]}")"                                                        \
444✔
921
                         "$(Color G "${#config_only_files[@]}")"
444✔
922

923
        #
924
        # Modified file properties
925
        #
926

927
        LogEnter 'Examining file properties...\n'
111✔
928

929
        LogEnter 'Loading data...\n'
111✔
930
        unset orig_file_props # Also populated by AconfCompileSystem, so that it can be used by AconfDefaultFileProp
111✔
931
        typeset -Ag output_file_props ; AconfReadFileProps "$output_dir"/file-props.txt output_file_props
222✔
932
        typeset -Ag system_file_props ; AconfReadFileProps "$system_dir"/file-props.txt system_file_props
222✔
933
        typeset -Ag   orig_file_props ; AconfReadFileProps "$system_dir"/orig-file-props.txt orig_file_props
222✔
934
        LogLeave
111✔
935

936
        typeset -ag all_file_property_kinds
111✔
937
        all_file_property_kinds=("${!file_property_kind_exists[@]}")
111✔
938
        Print0Array all_file_property_kinds | sort --zero-terminated | mapfile -t -d $'\0' all_file_property_kinds
333✔
939

940
        AconfCompareFileProps
111✔
941

942
        LogLeave 'Done (%s only in system, %s changed, %s only in config).\n'        \
443✔
943
                         "$(Color G "${#system_only_file_props[@]}")"                                        \
443✔
944
                         "$(Color G "${#changed_file_props[@]}")"                                                \
443✔
945
                         "$(Color G "${#config_only_file_props[@]}")"
443✔
946
}
947

948
# The *_packages arrays are passed by name,
949
# so ShellCheck thinks the variables are unused:
950
# shellcheck disable=2034
951

952
# Prepare configuration and system state
953
function AconfCompile() {
954
        LogEnter 'Collecting data...\n'
110✔
955

956
        # Configuration
957

958
        AconfCompileOutput
110✔
959

960
        # System
961

962
        AconfCompileSystem
110✔
963

964
        # Vars
965

966
        < "$output_dir"/packages.txt         sort --unique | mapfile -t                   packages
220✔
967
        < "$system_dir"/packages.txt         sort --unique | mapfile -t         installed_packages
220✔
968

969
        < "$output_dir"/foreign-packages.txt sort --unique | mapfile -t           foreign_packages
220✔
970
        < "$system_dir"/foreign-packages.txt sort --unique | mapfile -t installed_foreign_packages
220✔
971

972
        AconfAnalyzeFiles
110✔
973

974
        LogLeave # Collecting data
110✔
975
}
976

977
####################################################################################################
978

979
pacman_opts=("$PACMAN")
6,607✔
980
aurman_opts=(aurman)
6,607✔
981
pacaur_opts=(pacaur)
6,607✔
982
yaourt_opts=(yaourt)
6,607✔
983
yay_opts=(yay)
6,607✔
984
paru_opts=(paru)
6,607✔
985
aura_opts=(aura)
6,607✔
986
makepkg_opts=(makepkg)
6,607✔
987
diff_opts=(diff '--color=auto')
6,607✔
988

989
aur_helper=
6,607✔
990
aur_helpers=(aurman pacaur yaourt yay paru aura makepkg)
6,607✔
991

992
# Only aconfmgr can use makepkg under root
993
if [[ $EUID == 0 ]]
6,607✔
994
then
995
        aur_helper=makepkg
×
996
fi
997

998
function DetectAurHelper() {
999
        if [[ -n "$aur_helper" ]]
16✔
1000
        then
1001
                return
10✔
1002
        fi
1003

1004
        LogEnter 'Detecting AUR helper...\n'
6✔
1005

1006
        local helper
6✔
1007
        for helper in "${aur_helpers[@]}"
42✔
1008
        do
1009
                if hash "$helper" 2> /dev/null
42✔
1010
                then
1011
                        aur_helper=$helper
6✔
1012
                        LogLeave '%s... Yes\n' "$(Color C %s "$helper")"
12✔
1013
                        return
6✔
1014
                fi
1015
                Log '%s... No\n' "$(Color C %s "$helper")"
72✔
1016
        done
1017

1018
        Log 'Can'\''t find even makepkg!?\n'
×
1019
        Exit 1
×
1020
}
1021

1022
base_devel_installed=n
6,607✔
1023

1024
# Query AUR RPC API to find the package base for a given package name.
1025
# This is used to resolve virtual provides and split packages without
1026
# requiring auracle-git to be installed (which would create circular dependencies).
1027
function AconfQueryAURPackageBase() {
1028
        local package=$1
1✔
1029
        local aur_rpc_url="https://aur.archlinux.org/rpc/?v=5"
1✔
1030
        local pkg_base=""
1✔
1031

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

1036
        if [[ -n "$response" ]]
1✔
1037
        then
1038
                resultcount=$(printf '%s' "$response" | sed -n 's/.*"resultcount":\([0-9]*\).*/\1/p')
3✔
1039
                if [[ "$resultcount" -gt 0 ]]
1✔
1040
                then
1041
                        pkg_base=$(printf '%s' "$response" | grep -o '"PackageBase":"[^"]*"' | head -1 | sed 's/"PackageBase":"\([^"]*\)"/\1/')
×
1042
                fi
1043
        fi
1044

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

1050
                if [[ -n "$response" ]]
1✔
1051
                then
1052
                        resultcount=$(printf '%s' "$response" | sed -n 's/.*"resultcount":\([0-9]*\).*/\1/p')
3✔
1053
                        if [[ "$resultcount" -gt 0 ]]
1✔
1054
                        then
1055
                                pkg_base=$(printf '%s' "$response" | grep -o '"PackageBase":"[^"]*"' | head -1 | sed 's/"PackageBase":"\([^"]*\)"/\1/')
5✔
1056
                        fi
1057
                fi
1058
        fi
1059

1060
        printf '%s' "$pkg_base"
1✔
1061
}
1062

1063
function AconfMakePkg() {
1064
        local install=true
12✔
1065
        if [[ "$1" == --noinstall ]]
12✔
1066
        then
1067
                install=false
×
1068
                shift
×
1069
        fi
1070

1071
        local package="$1"
12✔
1072
        local asdeps="${2:-false}"
12✔
1073

1074
        LogEnter 'Building foreign package %s from source.\n' "$(Color M %q "$package")"
24✔
1075

1076
        # shellcheck disable=SC2174
1077
        mkdir --parents --mode=700 "$aur_dir"
12✔
1078
        if [[ $EUID == 0 ]]
12✔
1079
        then
1080
                chown -R "$makepkg_user": "$aur_dir"
×
1081
        fi
1082

1083
        local pkg_dir="$aur_dir"/"$package"
12✔
1084
        Log 'Using directory %s.\n' "$(Color C %q "$pkg_dir")"
24✔
1085

1086
        rm -rf "$pkg_dir"
12✔
1087
        mkdir --parents "$pkg_dir"
12✔
1088

1089
        # Needed to clone the AUR repo. Should be replaced with curl/tar.
1090
        AconfNeedProgram git git n
12✔
1091

1092
        if [[ $base_devel_installed == n ]]
12✔
1093
        then
1094
                LogEnter 'Making sure the %s package is installed...\n' "$(Color M base-devel)"
18✔
1095
                ParanoidConfirm ''
9✔
1096
                if ! "$PACMAN" --query --quiet base-devel > /dev/null 2>&1
9✔
1097
                then
1098
                        AconfInstallNative base-devel
1✔
1099
                fi
1100

1101
                LogLeave
9✔
1102
                base_devel_installed=y
9✔
1103
        fi
1104

1105
        LogEnter 'Cloning...\n'
12✔
1106
        git clone "https://aur.archlinux.org/$package.git" "$pkg_dir"
12✔
1107
        LogLeave
12✔
1108

1109
        if [[ ! -f "$pkg_dir"/PKGBUILD ]]
12✔
1110
        then
1111
                Log 'No package description file found!\n'
1✔
1112

1113
                if [[ "$package" == auracle-git ]]
1✔
1114
                then
1115
                        FatalError 'Failed to download aconfmgr dependency!\n'
×
1116
                fi
1117

1118
                LogEnter 'Assuming this package is part of a package base:\n'
1✔
1119

1120
                LogEnter 'Retrieving package info from AUR...\n'
1✔
1121
                local pkg_base
1✔
1122
                pkg_base=$(AconfQueryAURPackageBase "$package")
2✔
1123

1124
                if [[ -z "$pkg_base" ]]
1✔
1125
                then
1126
                        # Fallback to auracle if AUR RPC fails
1127
                        Log 'AUR RPC query failed, falling back to auracle...\n'
×
1128
                        AconfNeedProgram auracle auracle-git y
×
1129
                        pkg_base=$(auracle info --format '{pkgbase}' "$package")
1130
                fi
1131
                LogLeave 'Done, package base is %s.\n' "$(Color M %q "$pkg_base")"
2✔
1132

1133
                AconfMakePkg "$pkg_base" "$asdeps" # recurse
1✔
1134
                LogLeave # Package base
1✔
1135
                LogLeave # Package
1✔
1136
                return
1✔
1137
        fi
1138

1139
        AconfMakePkgDir "$package" "$asdeps" "$install" "$pkg_dir"
11✔
1140
}
1141

1142
function AconfMakePkgDir() {
1143
        local package=$1
11✔
1144
        local asdeps=$2
11✔
1145
        local install=$3
11✔
1146
        local pkg_dir=$4
11✔
1147

1148
        local gnupg_home
11✔
1149
        gnupg_home="$(realpath -m "$tmp_dir/gnupg")"
22✔
1150

1151
        local infofile infofilename
11✔
1152
        for infofilename in .SRCINFO .AURINFO
22✔
1153
        do
1154
                infofile="$pkg_dir"/"$infofilename"
22✔
1155
                if test -f "$infofile"
22✔
1156
                then
1157
                        LogEnter 'Checking dependencies...\n'
11✔
1158

1159
                        local depends missing_depends dependency arch
11✔
1160
                        arch="$(uname -m)"
22✔
1161
                        # Filter out packages from the same base
1162
                        ( grep -E $'^\t(make|check)?depends(_'"$arch"')? = ' "$infofile" || true ) \
15✔
1163
                                | sed 's/^.* = \([^<>=]*\)\([<>=].*\)\?$/\1/g' \
11✔
1164
                                | ( grep -vFf <(( grep '^pkgname = ' "$infofile" || true) \
33✔
1165
                                                                        | sed 's/^.* = \(.*\)$/\1/g' ) \
×
1166
                                                || true ) \
4✔
1167
                                | mapfile -t depends
11✔
1168

1169
                        if [[ ${#depends[@]} != 0 ]]
11✔
1170
                        then
1171
                                ( "$PACMAN" --deptest "${depends[@]}" || true ) | mapfile -t missing_depends
21✔
1172
                                if [[ ${#missing_depends[@]} != 0 ]]
7✔
1173
                                then
1174
                                        for dependency in "${missing_depends[@]}"
19✔
1175
                                        do
1176
                                                LogEnter '%s:\n' "$(Color M %q "$dependency")"
38✔
1177
                                                if "$PACMAN" --query --info "$dependency" > /dev/null 2>&1
19✔
1178
                                                then
1179
                                                        Log 'Already installed.\n' # Shouldn't happen, actually
×
1180
                                                elif "$PACMAN" --sync --info "$dependency" > /dev/null 2>&1
19✔
1181
                                                then
1182
                                                        Log 'Installing from repositories...\n'
15✔
1183
                                                        AconfInstallNative --asdeps "$dependency"
15✔
1184
                                                        Log 'Installed.\n'
15✔
1185
                                                else
1186
                                                        local installed=false
4✔
1187

1188
                                                        # Check if this package is provided by something in pacman repos.
1189
                                                        # `pacman -Si` will not give us that information,
1190
                                                        # however, `pacman -S` still works.
1191
                                                        AconfNeedProgram pacsift pacutils n
4✔
1192
                                                        AconfNeedProgram unbuffer expect n
4✔
1193
                                                        local providers
4✔
1194
                                                        providers=$(unbuffer pacsift --sync --exact --satisfies="$dependency")
8✔
1195
                                                        if [[ -n "$providers" ]]
4✔
1196
                                                        then
1197
                                                                Log 'Installing provider package from repositories...\n'
2✔
1198
                                                                AconfInstallNative --asdeps "$dependency"
2✔
1199
                                                                Log 'Installed.\n'
2✔
1200
                                                                installed=true
2✔
1201
                                                        fi
1202

1203
                                                        if ! $installed
4✔
1204
                                                        then
1205
                                                                Log 'Installing from AUR...\n'
2✔
1206
                                                                AconfMakePkg "$dependency" true
2✔
1207
                                                                Log 'Installed.\n'
2✔
1208
                                                        fi
1209
                                                fi
1210

1211
                                                LogLeave ''
19✔
1212
                                        done
1213
                                fi
1214
                        fi
1215

1216
                        LogLeave
11✔
1217

1218
                        local keys
11✔
1219
                        ( grep -E $'^\tvalidpgpkeys = ' "$infofile" || true ) | sed 's/^.* = \(.*\)$/\1/' | mapfile -t keys
43✔
1220
                        if [[ ${#keys[@]} != 0 ]]
11✔
1221
                        then
1222
                                LogEnter 'Checking PGP keys...\n'
1✔
1223

1224
                                local key
1✔
1225
                                for key in "${keys[@]}"
1✔
1226
                                do
1227
                                        export GNUPGHOME="$gnupg_home"
2✔
1228

1229
                                        if [[ ! -d "$GNUPGHOME" ]]
1✔
1230
                                        then
1231
                                                LogEnter 'Creating %s...\n' "$(Color C %s "$GNUPGHOME")"
2✔
1232
                                                mkdir --parents "$GNUPGHOME"
1✔
1233
                                                gpg --gen-key --batch <<EOF
1✔
1234
Key-Type: DSA
1✔
1235
Key-Length: 1024
1✔
1236
Name-Real: aconfmgr
1✔
1237
%no-protection
1✔
1238
EOF
1✔
1239
                                                LogLeave
1✔
1240
                                        fi
1✔
1241

1✔
1242
                                        LogEnter 'Adding key %s...\n' "$(Color Y %q "$key")"
2✔
1243
                                        #ParanoidConfirm ''
1✔
1244

1✔
1245
                                        local ok=false
1✔
1246
                                        local keyserver
1✔
1247
                                        for keyserver in keys.gnupg.net pgp.mit.edu pool.sks-keyservers.net keyserver.ubuntu.com # subkeys.pgp.net
1✔
1248
                                        do
1✔
1249
                                                LogEnter 'Trying keyserver %s...\n' "$(Color C %s "$keyserver")"
2✔
1250
                                                if gpg --keyserver "$keyserver" --recv-key "$key"
1✔
1251
                                                then
1✔
1252
                                                        ok=true
1✔
1253
                                                        LogLeave 'OK!\n'
1✔
1254
                                                        break
1✔
1255
                                                else
1✔
1256
                                                        LogLeave 'Error...\n'
1✔
1257
                                                fi
1✔
1258
                                        done
1✔
1259

1✔
1260
                                        if ! $ok
1✔
1261
                                        then
1✔
1262
                                                FatalError 'No keyservers succeeded.\n'
1✔
1263
                                        fi
1✔
1264

1✔
1265
                                        if [[ $EUID == 0 ]]
1✔
1266
                                        then
1✔
1267
                                                chmod 700 "$gnupg_home"
1✔
1268
                                                chown -R "$makepkg_user": "$gnupg_home"
1✔
1269
                                        fi
1✔
1270

1✔
1271
                                        LogLeave
1✔
1272
                                done
1✔
1273

1✔
1274
                                LogLeave
1✔
1275
                        fi
1✔
1276
                fi
1✔
1277
        done
1✔
1278

1✔
1279
        LogEnter 'Evaluating environment...\n'
11✔
1280
        local path
11✔
1281
        # shellcheck disable=SC2016
1✔
1282
        path=$(env -i sh -c 'source /etc/profile 1>&2 ; printf -- %s "$PATH"')
22✔
1283
        LogLeave
11✔
1284

1✔
1285
        LogEnter 'Building...\n'
11✔
1286
        (
1✔
1287
                cd "$pkg_dir"
11✔
1288
                mkdir --parents home
11✔
1289
                # Set CARGO_TARGET_DIR to avoid issues with Rust builds on mounted volumes
1✔
1290
                # This prevents "could not write output" errors when building Rust packages
1✔
1291
                local cargo_target_dir="/tmp/cargo-target-$$"
11✔
1292
                local args=(env -i "PATH=$path" "HOME=$PWD/home" "GNUPGHOME=$gnupg_home" "CARGO_TARGET_DIR=$cargo_target_dir" "${makepkg_opts[@]}")
22✔
1293

1✔
1294
                if [[ $EUID == 0 ]]
11✔
1295
                then
1✔
1296
                        chown -R "$makepkg_user": .
1✔
1297
                        setpriv --reuid="$makepkg_user" --regid="$makepkg_user" --clear-groups bash -c "GNUPGHOME=$(realpath ../../gnupg) $(printf ' %q' "${args[@]}")" 1>&2
1✔
1298

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

1✔
1304
                                # Filter out packages that don't exist (e.g., debug packages not built by default)
1✔
1305
                                local existing_pkgs=()
1✔
1306
                                local skipped_pkgs=()
1✔
1307
                                local pkg
1✔
1308
                                for pkg in "${pkglist[@]}"
1✔
1309
                                do
1✔
1310
                                        if [[ -f "$pkg" ]]
1✔
1311
                                        then
1✔
1312
                                                existing_pkgs+=("$pkg")
1✔
1313
                                        else
1✔
1314
                                                skipped_pkgs+=("$pkg")
1✔
1315
                                        fi
1✔
1316
                                done
1✔
1317

1✔
1318
                                if [[ ${#skipped_pkgs[@]} -gt 0 ]]
1✔
1319
                                then
1✔
1320
                                        Log 'Skipping %s non-existent package(s) from packagelist:\n' "$(Color G "${#skipped_pkgs[@]}")"
1✔
1321
                                        for pkg in "${skipped_pkgs[@]}"
1✔
1322
                                        do
1✔
1323
                                                Log '  %s\n' "$(Color M "%q" "$(basename "$pkg")")"
1✔
1324
                                        done
1✔
1325
                                fi
1✔
1326

1✔
1327
                                if [[ ${#existing_pkgs[@]} -eq 0 ]]
1✔
1328
                                then
1✔
1329
                                        Log 'Warning: No packages to install (all were filtered out)\n'
1✔
1330
                                elif $asdeps
1✔
1331
                                then
1✔
1332
                                        "${pacman_opts[@]}" --upgrade --asdeps "${existing_pkgs[@]}"
1✔
1333
                                else
1✔
1334
                                        "${pacman_opts[@]}" --upgrade "${existing_pkgs[@]}"
1✔
1335
                                fi
1✔
1336
                        fi
1✔
1337
                else
1✔
1338
                        if $asdeps
11✔
1339
                        then
1✔
1340
                                args+=(--asdeps)
4✔
1341
                        fi
1✔
1342

1✔
1343
                        if $install
11✔
1344
                        then
1✔
1345
                                args+=(--install)
11✔
1346
                        fi
11✔
1347

11✔
1348
                        "${args[@]}" 1>&2
11✔
1349
                fi
1✔
1350
        )
1✔
1351
        LogLeave
11✔
1352

1✔
1353
        LogLeave
11✔
1354
}
1✔
1355

1✔
1356
function AconfInstallNative() {
1✔
1357
        local asdeps=false asdeps_arr=()
48✔
1358
        if [[ "$1" == --asdeps ]]
24✔
1359
        then
1✔
1360
                asdeps=true
18✔
1361
                asdeps_arr=(--asdeps)
18✔
1362
                shift
18✔
1363
        fi
1✔
1364

1✔
1365
        local target_packages=("$@")
48✔
1366
        if [[ $prompt_mode == never ]]
24✔
1367
        then
1✔
1368
                # Some prompts default to 'no'
1✔
1369
                ( yes || true ) | sudo "${pacman_opts[@]}" --confirm --sync "${asdeps_arr[@]}" "${target_packages[@]}"
1✔
1370
        else
1✔
1371
                sudo "${pacman_opts[@]}" --sync "${asdeps_arr[@]}" "${target_packages[@]}"
24✔
1372
        fi
1✔
1373
}
1✔
1374

1✔
1375
function AconfInstallForeign() {
1✔
1376
        local asdeps=false asdeps_arr=()
20✔
1377
        if [[ "$1" == --asdeps ]]
10✔
1378
        then
1✔
1379
                asdeps=true
2✔
1380
                asdeps_arr=(--asdeps)
2✔
1381
                shift
2✔
1382
        fi
1✔
1383

1✔
1384
        local target_packages=("$@")
20✔
1385

1✔
1386
        DetectAurHelper
10✔
1387

1✔
1388
        case "$aur_helper" in
10✔
1389
                aurman)
1✔
1390
                        RunExternal "${aurman_opts[@]}" --sync --aur "${asdeps_arr[@]}" "${target_packages[@]}"
1✔
1391
                        ;;
1✔
1392
                pacaur)
1✔
1393
                        RunExternal "${pacaur_opts[@]}" --sync --aur "${asdeps_arr[@]}" "${target_packages[@]}"
2✔
1394
                        ;;
1✔
1395
                yaourt)
1✔
1396
                        RunExternal "${yaourt_opts[@]}" --sync --aur "${asdeps_arr[@]}" "${target_packages[@]}"
1✔
1397
                        ;;
1✔
1398
                yay)
1✔
1399
                        RunExternal "${yay_opts[@]}" --sync --aur "${asdeps_arr[@]}" "${target_packages[@]}"
2✔
1400
                        ;;
1✔
1401
                paru)
1✔
1402
                        RunExternal "${paru_opts[@]}" --sync --aur "${asdeps_arr[@]}" "${target_packages[@]}"
2✔
1403
                        ;;
1✔
1404
                aura)
1✔
1405
                        RunExternal "${aura_opts[@]}" -A "${asdeps_arr[@]}" "${target_packages[@]}"
1✔
1406
                        ;;
1✔
1407
                makepkg)
1✔
1408
                        local package
7✔
1409
                        for package in "${target_packages[@]}"
7✔
1410
                        do
1✔
1411
                                AconfMakePkg "$package" "$asdeps"
7✔
1412
                        done
1✔
1413
                        ;;
1✔
1414
                *)
1✔
1415
                        Log 'Error: unknown AUR helper %q\n' "$aur_helper"
1✔
1416
                        false
1✔
1417
                        ;;
1✔
1418
        esac
1✔
1419
}
1✔
1420

1✔
1421
function AconfNeedProgram() {
1✔
1422
        local program="$1" # program that needs to be in PATH
139✔
1423
        local package="$2" # package the program is available in
139✔
1424
        local foreign="$3" # whether this is a foreign package
139✔
1425

1✔
1426
        if ! hash "$program" 2> /dev/null
140✔
1427
        then
1✔
1428
                if [[ $foreign == y ]]
3✔
1429
                then
1✔
1430
                        LogEnter 'Installing foreign dependency %s:\n' "$(Color M %q "$package")"
3✔
1431
                        ParanoidConfirm ''
2✔
1432
                        AconfInstallForeign --asdeps "$package"
2✔
1433
                else
1✔
1434
                        LogEnter 'Installing native dependency %s:\n' "$(Color M %q "$package")"
3✔
1435
                        ParanoidConfirm ''
2✔
1436
                        AconfInstallNative --asdeps "$package"
2✔
1437
                fi
1✔
1438
                LogLeave 'Installed.\n'
3✔
1439
        fi
1✔
1440
}
1✔
1441

1✔
1442
# Get the path to the package file (.pkg.tar.*) for the specified package.
1✔
1443
# Download or build the package if necessary.
1✔
1444
function AconfNeedPackageFile() {
1✔
1445
        set -e
14✔
1446
        local package="$1"
14✔
1447

1✔
1448
        local info foreign
14✔
1449
        if info="$(LC_ALL=C "$PACMAN" --query --info "$package")"
42✔
1450
        then
1✔
1451
                if "$PACMAN" --query --quiet --foreign "$package" > /dev/null
11✔
1452
                then
1✔
1453
                        foreign=true
3✔
1454
                else
1✔
1455
                        foreign=false
9✔
1456
                fi
1✔
1457
        else
1✔
1458
                if info="$(LC_ALL=C "$PACMAN" --sync --info "$package")"
9✔
1459
                then
1✔
1460
                        foreign=false
1✔
1461
                else
1✔
1462
                        foreign=true
3✔
1463
                fi
1✔
1464
        fi
1✔
1465

1✔
1466
        local version='' architecture='' filemask_precise filemask_any
14✔
1467
        if [[ -n "$info" ]]
14✔
1468
        then
1✔
1469
                version="$(grep '^Version' <<< "$info" | sed 's/^.* : //g')"
33✔
1470
                architecture="$(grep '^Architecture' <<< "$info" | sed 's/^.* : //g')"
33✔
1471
                filemask_precise=$(printf "%q-%q-%q.pkg.*" "$package" "$version" "$architecture")
22✔
1472
        fi
1✔
1473
        filemask_any=$(printf "%q-*-*.pkg.*" "$package")
28✔
1474

1✔
1475
        # try without downloading first
1✔
1476
        local downloaded
14✔
1477
        for downloaded in false true
14✔
1478
        do
1✔
1479
                local precise
14✔
1480
                for precise in true false
17✔
1481
                do
1✔
1482
                        # if we don't have the exact version, we can only do non-precise
1✔
1483
                        if $precise && [[ -z "$version" ]]
31✔
1484
                        then
1✔
1485
                                continue
3✔
1486
                        fi
1✔
1487

1✔
1488
                        local filemask
14✔
1489
                        if $precise
14✔
1490
                        then
1✔
1491
                                filemask=$filemask_precise
11✔
1492
                        else
1✔
1493
                                filemask=$filemask_any
3✔
1494
                        fi
1✔
1495

1✔
1496
                        local dirs=()
28✔
1497
                        if $foreign
14✔
1498
                        then
1✔
1499
                                DetectAurHelper
6✔
1500
                                local -A tried_helper=()
12✔
1501

1✔
1502
                                local helper
6✔
1503
                                for helper in "$aur_helper" "${aur_helpers[@]}"
48✔
1504
                                do
1✔
1505
                                        if [[ ${tried_helper[$helper]+x} ]]
48✔
1506
                                        then
1✔
1507
                                                continue
6✔
1508
                                        fi
1✔
1509
                                        tried_helper[$helper]=y
42✔
1510

1✔
1511
                                        case "$helper" in
42✔
1512
                                                aurman)
1✔
1513
                                                        dirs+=("${XDG_CACHE_HOME:-$HOME/.cache}/aurman/$package")
6✔
1514
                                                        ;;
1✔
1515
                                                pacaur)
1✔
1516
                                                        dirs+=("${XDG_CACHE_HOME:-$HOME/.cache}/pacaur/$package")
6✔
1517
                                                        ;;
1✔
1518
                                                yaourt)
1✔
1519
                                                        # yaourt does not save .pkg.xz files
1✔
1520
                                                        ;;
1✔
1521
                                                yay)
1✔
1522
                                                        dirs+=("${XDG_CACHE_HOME:-$HOME/.cache}/yay/$package")
6✔
1523
                                                        ;;
1✔
1524
                                                paru)
1✔
1525
                                                        dirs+=("${XDG_CACHE_HOME:-$HOME/.cache}/paru/clone/$package")
6✔
1526
                                                        ;;
1✔
1527
                                                aura)
1✔
1528
                                                        dirs+=("${XDG_CACHE_HOME:-$HOME/.cache}/aura/cache")
6✔
1529
                                                        ;;
1✔
1530
                                                makepkg)
1✔
1531
                                                        dirs+=("$aur_dir"/"$package")
6✔
1532
                                                        ;;
1✔
1533
                                                *)
1✔
1534
                                                        Log 'Error: unknown AUR helper %q\n' "$aur_helper"
1✔
1535
                                                        false
1✔
1536
                                                        ;;
1✔
1537
                                        esac
1✔
1538
                                done
1✔
1539
                        else
1✔
1540
                                local dir
9✔
1541
                                ( LC_ALL=C pacman --verbose 2>/dev/null || true ) \
25✔
1542
                                        | sed -n 's/^Cache Dirs: \(.*\)$/\1/p' \
9✔
1543
                                        | sed 's/  /\n/g' \
9✔
1544
                                        | while read -r dir
25✔
1545
                                do
1✔
1546
                                        if [[ -n "$dir" ]]
17✔
1547
                                        then
1✔
1548
                                                dirs+=("$dir")
9✔
1549
                                        fi
1✔
1550
                                done
1✔
1551
                        fi
1✔
1552

1✔
1553
                        local files=()
28✔
1554
                        local dir
14✔
1555
                        for dir in "${dirs[@]}"
44✔
1556
                        do
1✔
1557
                                if sudo test -d "$dir"
44✔
1558
                                then
1✔
1559
                                        sudo find "$dir" -type f -name "$filemask" -not -name '*.sig' -print0 | \
14✔
1560
                                                while read -r -d $'\0' file
28✔
1561
                                                do
1✔
1562
                                                        files+=("$file")
14✔
1563
                                                done
1✔
1564
                                fi
1✔
1565
                        done
1✔
1566

1✔
1567
                        local file
14✔
1568
                        for file in "${files[@]}"
14✔
1569
                        do
1✔
1570
                                local correct
14✔
1571
                                if $precise
14✔
1572
                                then
1✔
1573
                                        correct=true
11✔
1574
                                else
1✔
1575
                                        local pkgname
3✔
1576
                                        pkgname=$(bsdtar -x --to-stdout --file "$file" .PKGINFO | \
1✔
1577
                                                                  sed -n 's/^pkgname = \(.*\)$/\1/p')
9✔
1578
                                        if [[ "$pkgname" == "$package" ]]
3✔
1579
                                        then
1✔
1580
                                                correct=true
3✔
1581
                                        else
1✔
1582
                                                correct=false
1✔
1583
                                        fi
1✔
1584
                                fi
1✔
1585

1✔
1586
                                if $correct
14✔
1587
                                then
1✔
1588
                                        printf '%s' "$file"
14✔
1589
                                        return
14✔
1590
                                fi
1✔
1591
                        done
1✔
1592
                done
1✔
1593

1✔
1594
                if $downloaded
1✔
1595
                then
1✔
1596
                        Log 'Unable to find package file for package %s!\n' "$(Color M %q "$package")"
1✔
1597
                        Exit 1
1✔
1598
                else
1✔
1599
                        if $foreign
1✔
1600
                        then
1✔
1601
                                LogEnter 'Building foreign package %s\n' "$(Color M %q "$package")"
1✔
1602
                                ParanoidConfirm ''
1✔
1603

1✔
1604
                                local helper
1✔
1605
                                for helper in "$aur_helper" "${aur_helpers[@]}"
1✔
1606
                                do
1✔
1607
                                        case "$helper" in
1✔
1608
                                                aurman)
1✔
1609
                                                        # aurman does not have a --makepkg option
1✔
1610
                                                        ;;
1✔
1611
                                                pacaur)
1✔
1612
                                                        if command -v "${pacaur_opts[0]}" > /dev/null
1✔
1613
                                                        then
1✔
1614
                                                                RunExternal "${pacaur_opts[@]}" --makepkg --aur --makepkg "$package" 1>&2
1✔
1615
                                                                break
1✔
1616
                                                        fi
1✔
1617
                                                        ;;
1✔
1618
                                                yaourt)
1✔
1619
                                                        # yaourt does not save .pkg.xz files
1✔
1620
                                                        continue
1✔
1621
                                                        ;;
1✔
1622
                                                yay)
1✔
1623
                                                        # yay does not have a --makepkg option
1✔
1624
                                                        continue
1✔
1625
                                                        ;;
1✔
1626
                                                paru)
1✔
1627
                                                        # paru does not have a --makepkg option
1✔
1628
                                                        continue
1✔
1629
                                                        ;;
1✔
1630
                                                aura)
1✔
1631
                                                        # aura does not have a --makepkg option
1✔
1632
                                                        continue
1✔
1633
                                                        ;;
1✔
1634
                                                makepkg)
1✔
1635
                                                        AconfMakePkg --noinstall "$package"
1✔
1636
                                                        break
1✔
1637
                                                        ;;
1✔
1638
                                                *)
1✔
1639
                                                        Log 'Error: unknown AUR helper %q\n' "$aur_helper"
1✔
1640
                                                        false
1✔
1641
                                                        ;;
1✔
1642
                                        esac
1✔
1643
                                done
1✔
1644

1✔
1645
                                LogLeave
1✔
1646
                        else
1✔
1647
                                LogEnter "Downloading package %s (%s) to pacman's cache\\n" "$(Color M %q "$package")" "$(Color C %s "$filemask_precise")"
1✔
1648
                                ParanoidConfirm ''
1✔
1649
                                sudo "$PACMAN" --sync --download --nodeps --nodeps --noconfirm "$package" 1>&2
1✔
1650
                                LogLeave
1✔
1651
                        fi
1✔
1652
                fi
1✔
1653
        done
1✔
1654
}
1✔
1655

1✔
1656
# Extract the original file from a package to stdout
1✔
1657
function AconfGetPackageOriginalFile() {
1✔
1658
        local package="$1" # Package to extract the file from
14✔
1659
        local file="$2" # Absolute path to file in package
14✔
1660

1✔
1661
        local package_file
14✔
1662
        package_file="$(AconfNeedPackageFile "$package")"
28✔
1663

1✔
1664
        local args=(bsdtar -x --to-stdout --file "$package_file" "${file/\//}")
28✔
1665
        if [[ -r "$package_file" ]]
14✔
1666
        then
6✔
1667
                "${args[@]}"
7✔
1668
        else
1✔
1669
                sudo "${args[@]}"
8✔
1670
        fi
1✔
1671
}
1✔
1672

1✔
1673
function AconfRestoreFile() {
1✔
1674
        local package=$1
1✔
1675
        local file=$2
1✔
1676

1✔
1677
        local package_file
1✔
1678
        package_file="$(AconfNeedPackageFile "$package")"
1✔
1679

1✔
1680
        # If we are restoring a directory, it may be non-empty.
1✔
1681
        # Extract the object to a temporary location first.
1✔
1682
        local tmp_base=${tmp_dir:?}/dir-props
1✔
1683
        sudo rm -rf "$tmp_base"
1✔
1684

1✔
1685
        mkdir -p "$tmp_base"
1✔
1686
        local tmp_file="$tmp_base""$file"
1✔
1687
        sudo tar x --directory "$tmp_base" --file "$package_file" --no-recursion "${file/\//}"
1✔
1688

1✔
1689
        AconfReplace "$tmp_file" "$file"
1✔
1690
        sudo rm -rf "$tmp_base"
1✔
1691
}
1✔
1692

1✔
1693
# Move filesystem object at $1 to $2, replacing any existing one.
1✔
1694
# Attempt to do so atomically, when possible.
1✔
1695
# Do the right thing when filesystem objects differ, but never
1✔
1696
# recursively remove directories (copy their attributes instead).
1✔
1697
function AconfReplace() {
1✔
1698
        local src=$1
1✔
1699
        local dst=$2
1✔
1700

1✔
1701
        # Try direct mv first
1✔
1702
        if ! sudo mv --no-target-directory "$src" "$dst" 2>/dev/null
1✔
1703
        then
1✔
1704
                # Direct mv failed - directory or object type mismatch
1✔
1705
                if sudo rm --force --dir "$dst" 2>/dev/null
1✔
1706
                then
1✔
1707
                        # Deleted target successfully, now overwrite it
1✔
1708
                        sudo mv --no-target-directory "$src" "$dst"
1✔
1709
                else
1✔
1710
                        # rm failed - likely a non-empty directory; copy
1✔
1711
                        # attributes only
1✔
1712
                        sudo chmod --reference="$src" "$dst"
1✔
1713
                        sudo chown --reference="$src" "$dst"
1✔
1714
                        sudo touch --reference="$src" "$dst"
1✔
1715
                fi
1✔
1716
        fi
1✔
1717
}
1✔
1718

1✔
1719
####################################################################################################
1✔
1720

1✔
1721
prompt_mode=normal # never / normal / paranoid
6,608✔
1722

1✔
1723
function Confirm() {
1✔
1724
        local detail_func="$1"
1✔
1725

1✔
1726
        if [[ $prompt_mode == never ]]
1✔
1727
        then
1✔
1728
                return
1✔
1729
        fi
1✔
1730

1✔
1731
        while true
1✔
1732
        do
1✔
1733
                if [[ -n "$detail_func" ]]
1✔
1734
                then
1✔
1735
                        Log 'Proceed? [Y/n/d] '
1✔
1736
                else
1✔
1737
                        Log 'Proceed? [Y/n] '
1✔
1738
                fi
1✔
1739
                read -r -n 1 answer < /dev/tty
1✔
1740
                echo 1>&2
1✔
1741
                case "$answer" in
1✔
1742
                        Y|y|'')
1✔
1743
                                return
1✔
1744
                                ;;
1✔
1745
                        N|n)
1✔
1746
                                Log '%s\n' "$(Color R "User abort")"
1✔
1747
                                Exit 1
1✔
1748
                                ;;
1✔
1749
                        D|d)
1✔
1750
                                $detail_func
1✔
1751
                                continue
1✔
1752
                                ;;
1✔
1753
                        *)
1✔
1754
                                continue
1✔
1755
                                ;;
1✔
1756
                esac
1✔
1757
        done
1✔
1758
}
1✔
1759

1✔
1760
function ParanoidConfirm() {
1✔
1761
        if [[ $prompt_mode == paranoid ]]
95✔
1762
        then
1✔
1763
                Confirm "$@"
94✔
1764
        fi
1✔
1765
}
1✔
1766

1✔
1767
####################################################################################################
1✔
1768

1✔
1769
log_indent=:
6,608✔
1770

1✔
1771
function Log() {
1✔
1772
        if [[ "$#" != 0 && -n "$1" ]]
81,664✔
1773
        then
1✔
1774
                local fmt="$1"
40,331✔
1775
                shift
40,326✔
1776

1✔
1777
                if [[ -z $ANSI_clear_line ]]
40,260✔
1778
                then
1✔
1779
                        # Replace carriage returns in format string with newline
1✔
1780
                        # when colors are disabled. This avoids systemd's journal
1✔
1781
                        # from showing such lines as [# blob data].
1✔
1782

1✔
1783
                        fmt=${fmt//\\r/\\n} # Replace the '\r' sequence
1✔
1784
                                            # (backslash-r) , not actual carriage
1✔
1785
                                            # returns.
1✔
1786
                fi
1✔
1787

1✔
1788
                printf "${ANSI_clear_line}${ANSI_color_B}%s ${ANSI_color_W}${fmt}${ANSI_reset}" "$log_indent" "$@" 1>&2
40,224✔
1789
        fi
1✔
1790
}
1✔
1791

1✔
1792
function LogEnter() {
1✔
1793
        Log "$@"
3,981✔
1794
        log_indent=$log_indent:
3,948✔
1795
}
1✔
1796

1✔
1797
function LogLeave() {
1✔
1798
        if [[ $# == 0 ]]
3,986✔
1799
        then
1✔
1800
                Log 'Done.\n'
2,505✔
1801
        else
1✔
1802
                Log "$@"
1,481✔
1803
        fi
1✔
1804

1✔
1805
        log_indent=${log_indent::-1}
3,986✔
1806
}
1✔
1807

1✔
1808
function ConfigWarning() {
1✔
1809
        Log '%s: '"$1" "$(Color Y "Warning")" "${@:2}"
13✔
1810
        printf W >> "$output_dir"/warnings
7✔
1811
}
1✔
1812

1✔
1813
function FatalError() {
1✔
1814
        Log "$@"
49✔
1815
        false
97✔
1816
        # if we're here, errexit is not set
1✔
1817
        Log 'Continuing after error. This is a bug, please report it.\n'
1✔
1818
        Exit 1
1✔
1819
}
1✔
1820

1✔
1821
function Color() {
1✔
1822
        local var="ANSI_color_$1"
35,404✔
1823
        printf -- "%s" "${!var}"
35,375✔
1824
        shift
35,383✔
1825
        # shellcheck disable=2059
1✔
1826
        printf -- "$@"
35,317✔
1827
        printf -- "%s" "${ANSI_color_W}"
35,285✔
1828
}
1✔
1829

1✔
1830
# The ANSI_color_* variables are looked up by name:
1✔
1831
# shellcheck disable=2034
1✔
1832
function DisableColor() {
1✔
1833
        ANSI_color_R=
1✔
1834
        ANSI_color_G=
1✔
1835
        ANSI_color_Y=
1✔
1836
        ANSI_color_B=
1✔
1837
        ANSI_color_M=
1✔
1838
        ANSI_color_C=
1✔
1839
        ANSI_color_W=
1✔
1840
        ANSI_reset=
1✔
1841
        ANSI_clear_line=
1✔
1842
}
1✔
1843

1✔
1844
####################################################################################################
1✔
1845

1✔
1846
# shellcheck disable=SC2329  # This function is invoked indirectly via trap
1✔
1847
function OnError() {
1✔
1848
        trap '' EXIT ERR
51✔
1849

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

1✔
1852
        local frame=0 str
51✔
1853
        # shellcheck disable=SC2086  # frame is a controlled integer, no risk of word splitting
1✔
1854
        while str=$(caller $frame)
397✔
1855
        do
1✔
1856
                if [[ $str =~ ^([^\ ]*)\ ([^\ ]*)\ (.*)$ ]]
149✔
1857
                then
1✔
1858
                        Log '%s:%s [%s]\n' "$(Color C "%q" "${BASH_REMATCH[3]}")" "$(Color G "%q" "${BASH_REMATCH[1]}")" "$(Color Y "%q" "${BASH_REMATCH[2]}")"
593✔
1859
                else
1✔
1860
                        Log '%s\n' "$str"
1✔
1861
                fi
1✔
1862

1✔
1863
                frame=$((frame+1))
149✔
1864
        done
1✔
1865

1✔
1866
        LogLeave ''
51✔
1867

1✔
1868
        if [[ -d "$tmp_dir" ]]
51✔
1869
        then
1✔
1870
                local df dir
51✔
1871
                df=$(($(stat -f --format="%a*%S" "$tmp_dir")))
101✔
1872
                if ! dir="$(realpath "$(dirname "$tmp_dir")" 2> /dev/null)"
151✔
1873
                then
1✔
1874
                        dir="$(dirname "$tmp_dir")"
1✔
1875
                fi
1✔
1876
                if [[ $df -lt $warn_tmp_df_threshold ]]
51✔
1877
                then
1✔
1878
                        LogEnter 'Probable cause: low disk space (%s bytes) in %s. Suggestions:\n' "$(Color G %s "$df")" "$(Color C %q "$dir")"
1✔
1879
                        Log '- Ignore more files and directories using %s directives;\n' "$(Color Y IgnorePath)"
1✔
1880
                        Log '- Free up more space in %s;\n' "$(Color C %q "$dir")"
1✔
1881
                        Log '- Set %s to another location before invoking %s.\n' "$(Color Y \$TMPDIR)" "$(Color Y aconfmgr)"
1✔
1882
                        LogLeave ''
1✔
1883
                fi
1✔
1884
        fi
1✔
1885

1✔
1886
        # Ensure complete abort when inside a string expansion
1✔
1887
        exit 1
51✔
1888
}
1✔
1889
trap OnError EXIT ERR
6,608✔
1890

1✔
1891
function Exit() {
1✔
1892
        trap '' EXIT ERR
6,558✔
1893
        exit "${1:-0}"
6,558✔
1894
}
1✔
1895

1✔
1896
####################################################################################################
1✔
1897

1✔
1898
# Print an array, one element per line (assuming IFS starts with \n).
1✔
1899
function PrintArray() {
1✔
1900
        local name="$1" # Name of the global variable containing the array
1,475✔
1901
        local size
1,475✔
1902

1✔
1903
        size="$(eval "echo \${#${name}""[*]}")"
4,425✔
1904
        if [[ $size != 0 ]]
1,475✔
1905
        then
1✔
1906
                eval "echo \"\${${name}[*]}\""
910✔
1907
        fi
1✔
1908
}
1✔
1909

1✔
1910
# Ditto, but terminate elements with a NUL.
1✔
1911
function Print0Array() {
1✔
1912
        local name="$1" # Name of the global variable containing the array
1,393✔
1913

1✔
1914
        eval "$(cat <<EOF
1,557✔
1915
        if [[ \${#${name}[*]} != 0 ]]
1,516✔
1916
        then
345✔
1917
                local item
3,541✔
1918
                for item in "\${${name}[@]}"
1,516✔
1919
                do
3,536✔
1920
                        printf '%s\\0' "\$item"
1,516✔
1921
                done
1,516✔
1922
        fi
1,516✔
1923
EOF
1,516✔
1924
)"
2,623✔
1925
}
1926

604✔
1927
# Ditto, but shell-escape array elements.
1,842✔
1928
# shellcheck disable=SC2329  # This is a utility function called indirectly
1929
function PrintQArray() {
1,840✔
1930
        local name="$1" # Name of the global variable containing the array
453✔
1931
        local size
453✔
1932

1933
        size="$(eval "echo \${#${name}""[*]}")"
1,359✔
1934
        if [[ $size != 0 ]]
453✔
1935
        then
1936
                eval "printf -- %q \"\${${name}[0]}\""
6✔
1937
                if [[ $size -gt 1 ]]
3✔
1938
                then
1939
                        eval "printf -- ' %q' \"\${${name}[@]:1}\""
2✔
1940
                fi
1941
        fi
1942
}
1943

1944
if [[ $EUID == 0 ]]
6,608✔
1945
then
1946
        function sudo() { "$@" ; }
1947
fi
1948

1949
# Run external bash script.
1950
# No-op, except when running under the test suite with bashcov,
1951
# in which case it does not propagate bashcov to the invoked script.
1952
# Unlike `env -i`, does not nuke the environment.
1953
function RunExternal() {
1954
        env -u SHELLOPTS -u PS4 -u SHLVL -u BASH_XTRACEFD "$@"
4✔
1955
}
1956

1957
# cat a file; if it's not readable, cat via sudo.
1958
# shellcheck disable=SC2329  # This is a utility function called indirectly
1959
function SuperCat() {
1960
        local file="$1"
1✔
1961

1962
        if [[ -r "$1" ]]
1✔
1963
        then
1964
                cat "$1"
×
1965
        else
1966
                sudo cat "$1"
1✔
1967
        fi
1968
}
1969

1970
if ! ( empty_array=() ; : "${empty_array[@]}" )
13,216✔
1971
then
1972
        # Old bash versions treat substitution of an empty array
1973
        # synonymous with substituting an unset variable, signaling an
1974
        # error in -u mode and stopping the script in -e mode. We
1975
        # generally want both of these enabled to catch bugs early and
1976
        # fail fast, but making every array substitution conditional on
1977
        # whether it is empty or not is unreasonably onerous, so just
1978
        # disable those checks in old bash versions - it is then up to the
1979
        # test suite ran against newer bash versions to ensure code
1980
        # correctness.
1981

1982
        Log '%s: Old bash detected, disabling unset variable checking.\n' "$(Color Y "Warning")"
×
1983
        set +u
×
1984
fi
1985

1986
: # include in coverage
6,608✔
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

© 2026 Coveralls, Inc