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

systemd / systemd / 16280725298

14 Jul 2025 08:16PM UTC coverage: 72.166% (-0.006%) from 72.172%
16280725298

push

github

web-flow
Two fixlets for coverage test (#38183)

302135 of 418667 relevant lines covered (72.17%)

773261.64 hits per line

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

57.06
/src/bootctl/bootctl-status.c
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2

3
#include <fnmatch.h>
4
#include <unistd.h>
5

6
#include "sd-varlink.h"
7

8
#include "alloc-util.h"
9
#include "bootctl.h"
10
#include "bootctl-status.h"
11
#include "bootctl-util.h"
12
#include "bootspec.h"
13
#include "chase.h"
14
#include "devnum-util.h"
15
#include "dirent-util.h"
16
#include "efi-api.h"
17
#include "efi-loader.h"
18
#include "efivars.h"
19
#include "errno-util.h"
20
#include "fd-util.h"
21
#include "hashmap.h"
22
#include "log.h"
23
#include "pager.h"
24
#include "path-util.h"
25
#include "pretty-print.h"
26
#include "recurse-dir.h"
27
#include "string-util.h"
28
#include "strv.h"
29
#include "tpm2-util.h"
30

31
static int boot_config_load_and_select(
28✔
32
                BootConfig *config,
33
                const char *esp_path,
34
                dev_t esp_devid,
35
                const char *xbootldr_path,
36
                dev_t xbootldr_devid) {
37

38
        int r;
28✔
39

40
        /* If XBOOTLDR and ESP actually refer to the same block device, suppress XBOOTLDR, since it would
41
         * find the same entries twice. */
42
        bool same = esp_path && xbootldr_path && devnum_set_and_equal(esp_devid, xbootldr_devid);
28✔
43

44
        r = boot_config_load(config, esp_path, same ? NULL : xbootldr_path);
28✔
45
        if (r < 0)
28✔
46
                return r;
47

48
        if (!arg_root) {
28✔
49
                _cleanup_strv_free_ char **efi_entries = NULL;
×
50

51
                r = efi_loader_get_entries(&efi_entries);
14✔
52
                if (r == -ENOENT || ERRNO_IS_NEG_NOT_SUPPORTED(r))
14✔
53
                        log_debug_errno(r, "Boot loader reported no entries.");
8✔
54
                else if (r < 0)
6✔
55
                        log_warning_errno(r, "Failed to determine entries reported by boot loader, ignoring: %m");
14✔
56
                else
57
                        (void) boot_config_augment_from_loader(config, efi_entries, /* auto_only= */ false);
6✔
58
        }
59

60
        return boot_config_select_special_entries(config, /* skip_efivars= */ !!arg_root);
28✔
61
}
62

63
static int status_entries(
15✔
64
                const BootConfig *config,
65
                const char *esp_path,
66
                sd_id128_t esp_partition_uuid,
67
                const char *xbootldr_path,
68
                sd_id128_t xbootldr_partition_uuid) {
69

70
        int r;
15✔
71

72
        assert(config);
15✔
73
        assert(esp_path || xbootldr_path);
15✔
74

75
        printf("%sBoot Loader Entry Locations:%s\n", ansi_underline(), ansi_normal());
30✔
76

77
        printf("          ESP: %s (", esp_path);
15✔
78
        if (!sd_id128_is_null(esp_partition_uuid))
18✔
79
                printf("/dev/disk/by-partuuid/" SD_ID128_UUID_FORMAT_STR "",
12✔
80
                       SD_ID128_FORMAT_VAL(esp_partition_uuid));
12✔
81
        if (!xbootldr_path)
15✔
82
                /* ESP is $BOOT if XBOOTLDR not present. */
83
                printf(", %s$BOOT%s", ansi_green(), ansi_normal());
18✔
84
        printf(")");
15✔
85

86
        if (xbootldr_path) {
15✔
87
                printf("\n     XBOOTLDR: %s (", xbootldr_path);
6✔
88
                if (!sd_id128_is_null(xbootldr_partition_uuid))
9✔
89
                        printf("/dev/disk/by-partuuid/" SD_ID128_UUID_FORMAT_STR ", ",
3✔
90
                               SD_ID128_FORMAT_VAL(xbootldr_partition_uuid));
3✔
91
                /* XBOOTLDR is always $BOOT if present. */
92
                printf("%s$BOOT%s)", ansi_green(), ansi_normal());
12✔
93
        }
94

95
        if (settle_entry_token() >= 0)
15✔
96
                printf("\n        token: %s", arg_entry_token);
15✔
97
        printf("\n\n");
15✔
98

99
        if (config->default_entry < 0)
15✔
100
                printf("%zu entries, no entry could be determined as default.\n", config->n_entries);
6✔
101
        else {
102
                printf("%sDefault Boot Loader Entry:%s\n", ansi_underline(), ansi_normal());
18✔
103

104
                r = show_boot_entry(
9✔
105
                                boot_config_default_entry(config),
106
                                /* show_as_default= */ false,
107
                                /* show_as_selected= */ false,
108
                                /* show_reported= */ false);
109
                if (r > 0)
9✔
110
                        /* < 0 is already logged by the function itself, let's just emit an extra warning if
111
                           the default entry is broken */
112
                        printf("\nWARNING: default boot entry is broken\n");
×
113
        }
114

115
        return 0;
15✔
116
}
117

118
static int print_efi_option(uint16_t id, int *n_printed, bool in_order) {
12✔
119
        _cleanup_free_ char *title = NULL;
24✔
120
        _cleanup_free_ char *path = NULL;
12✔
121
        sd_id128_t partition;
12✔
122
        bool active;
12✔
123
        int r;
12✔
124

125
        assert(n_printed);
12✔
126

127
        r = efi_get_boot_option(id, &title, &partition, &path, &active);
12✔
128
        if (r == -ENOENT) {
12✔
129
                log_debug_errno(r, "Boot option 0x%04X referenced but missing, ignoring: %m", id);
×
130
                return 0;
×
131
        }
132
        if (r < 0)
12✔
133
                return log_error_errno(r, "Failed to read boot option 0x%04X: %m", id);
×
134

135
        /* print only configured entries with partition information */
136
        if (!path || sd_id128_is_null(partition)) {
12✔
137
                log_debug("Ignoring boot entry 0x%04X without partition information.", id);
12✔
138
                return 0;
12✔
139
        }
140

141
        efi_tilt_backslashes(path);
×
142

143
        if (*n_printed == 0) /* Print section title before first entry */
×
144
                printf("%sBoot Loaders Listed in EFI Variables:%s\n", ansi_underline(), ansi_normal());
×
145

146
        printf("        Title: %s%s%s\n", ansi_highlight(), strna(title), ansi_normal());
×
147
        printf("           ID: 0x%04X\n", id);
×
148
        printf("       Status: %sactive%s\n", active ? "" : "in", in_order ? ", boot-order" : "");
×
149
        printf("    Partition: /dev/disk/by-partuuid/" SD_ID128_UUID_FORMAT_STR "\n",
×
150
               SD_ID128_FORMAT_VAL(partition));
×
151
        printf("         File: %s%s%s/%s%s\n",
×
152
               glyph(GLYPH_TREE_RIGHT), ansi_grey(), arg_esp_path, ansi_normal(), path);
153
        printf("\n");
×
154

155
        (*n_printed)++;
×
156
        return 1;
×
157
}
158

159
static int status_variables(void) {
6✔
160
        _cleanup_free_ uint16_t *options = NULL, *order = NULL;
6✔
161
        int n_options, n_order, n_printed = 0;
6✔
162

163
        n_options = efi_get_boot_options(&options);
6✔
164
        if (n_options == -ENOENT)
6✔
165
                return log_error_errno(n_options,
×
166
                                       "Failed to access EFI variables, efivarfs"
167
                                       " needs to be available at /sys/firmware/efi/efivars/.");
168
        if (n_options < 0)
6✔
169
                return log_error_errno(n_options, "Failed to read EFI boot entries: %m");
×
170

171
        n_order = efi_get_boot_order(&order);
6✔
172
        if (n_order == -ENOENT)
6✔
173
                n_order = 0;
174
        else if (n_order < 0)
6✔
175
                return log_error_errno(n_order, "Failed to read EFI boot order: %m");
×
176

177
        /* print entries in BootOrder first */
178
        for (int i = 0; i < n_order; i++)
18✔
179
                (void) print_efi_option(order[i], &n_printed, /* in_order= */ true);
12✔
180

181
        /* print remaining entries */
182
        for (int i = 0; i < n_options; i++) {
18✔
183
                for (int j = 0; j < n_order; j++)
18✔
184
                        if (options[i] == order[j])
18✔
185
                                goto next_option;
12✔
186

187
                (void) print_efi_option(options[i], &n_printed, /* in_order= */ false);
×
188

189
        next_option:
12✔
190
                continue;
12✔
191
        }
192

193
        if (n_printed == 0)
6✔
194
                printf("No boot loaders listed in EFI Variables.\n\n");
6✔
195

196
        return 0;
197
}
198

199
static int enumerate_binaries(
30✔
200
                const char *esp_path,
201
                const char *path,
202
                char **previous,
203
                bool *is_first) {
204

205
        _cleanup_closedir_ DIR *d = NULL;
30✔
206
        _cleanup_free_ char *p = NULL;
30✔
207
        int c = 0, r;
30✔
208

209
        assert(esp_path);
30✔
210
        assert(path);
30✔
211
        assert(previous);
30✔
212
        assert(is_first);
30✔
213

214
        r = chase_and_opendir(path, esp_path, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, &p, &d);
30✔
215
        if (r == -ENOENT)
30✔
216
                return 0;
217
        if (r < 0)
30✔
218
                return log_error_errno(r, "Failed to read \"%s/%s\": %m", esp_path, skip_leading_slash(path));
×
219

220
        FOREACH_DIRENT(de, d, break) {
141✔
221
                _cleanup_free_ char *v = NULL, *filename = NULL;
51✔
222
                _cleanup_close_ int fd = -EBADF;
51✔
223

224
                if (!endswith_no_case(de->d_name, ".efi"))
51✔
225
                        continue;
×
226

227
                filename = path_join(p, de->d_name);
51✔
228
                if (!filename)
51✔
229
                        return log_oom();
×
230
                LOG_SET_PREFIX(filename);
102✔
231

232
                fd = openat(dirfd(d), de->d_name, O_RDONLY|O_CLOEXEC);
51✔
233
                if (fd < 0)
51✔
234
                        return log_error_errno(errno, "Failed to open file for reading: %m");
×
235

236
                r = get_file_version(fd, &v);
51✔
237
                if (r < 0 && r != -ESRCH)
51✔
238
                        return r;
239

240
                if (*previous) { /* Let's output the previous entry now, since now we know that there will be
51✔
241
                                  * one more, and can draw the tree glyph properly. */
242
                        printf("         %s %s%s\n",
72✔
243
                               *is_first ? "File:" : "     ",
36✔
244
                               glyph(GLYPH_TREE_BRANCH), *previous);
245
                        *is_first = false;
36✔
246
                        *previous = mfree(*previous);
36✔
247
                }
248

249
                /* Do not output this entry immediately, but store what should be printed in a state
250
                 * variable, because we only will know the tree glyph to print (branch or final edge) once we
251
                 * read one more entry */
252
                if (r == -ESRCH) /* No systemd-owned file but still interesting to print */
51✔
253
                        r = asprintf(previous, "%s%s/%s/%s/%s",
×
254
                                     ansi_grey(), esp_path, ansi_normal(), path, de->d_name);
255
                else /* if (r >= 0) */
256
                        r = asprintf(previous, "%s%s/%s/%s/%s (%s%s%s)",
153✔
257
                                     ansi_grey(), esp_path, ansi_normal(), path, de->d_name,
258
                                     ansi_highlight(), v, ansi_normal());
259
                if (r < 0)
51✔
260
                        return log_oom();
×
261

262
                c++;
51✔
263
        }
264

265
        return c;
266
}
267

268
static int status_binaries(const char *esp_path, sd_id128_t partition) {
15✔
269
        _cleanup_free_ char *last = NULL;
15✔
270
        bool is_first = true;
15✔
271
        int r, k;
15✔
272

273
        printf("%sAvailable Boot Loaders on ESP:%s\n", ansi_underline(), ansi_normal());
30✔
274

275
        if (!esp_path) {
15✔
276
                printf("          ESP: Cannot find or access mount point of ESP.\n\n");
×
277
                return -ENOENT;
278
        }
279

280
        printf("          ESP: %s", esp_path);
15✔
281
        if (!sd_id128_is_null(partition))
18✔
282
                printf(" (/dev/disk/by-partuuid/" SD_ID128_UUID_FORMAT_STR ")", SD_ID128_FORMAT_VAL(partition));
12✔
283
        printf("\n");
15✔
284

285
        r = enumerate_binaries(esp_path, "EFI/systemd", &last, &is_first);
15✔
286
        if (r < 0)
15✔
287
                goto fail;
×
288

289
        k = enumerate_binaries(esp_path, "EFI/BOOT", &last, &is_first);
15✔
290
        if (k < 0) {
15✔
291
                r = k;
×
292
                goto fail;
×
293
        }
294

295
        if (last) /* let's output the last entry now, since now we know that there will be no more, and can draw the tree glyph properly */
15✔
296
                printf("         %s %s%s\n",
30✔
297
                       is_first ? "File:" : "     ",
15✔
298
                       glyph(GLYPH_TREE_RIGHT), last);
299

300
        if (r == 0 && !arg_quiet)
15✔
301
                log_info("systemd-boot not installed in ESP.");
×
302
        if (k == 0 && !arg_quiet)
15✔
303
                log_info("No default/fallback boot loader installed in ESP.");
×
304

305
        printf("\n");
15✔
306
        return 0;
307

308
fail:
×
309
        errno = -r;
×
310
        printf("         File: (can't access %s: %m)\n\n", esp_path);
×
311
        return r;
312
}
313

314
static int efi_get_variable_string_and_warn(const char *variable, char **ret) {
66✔
315
        int r;
66✔
316

317
        r = efi_get_variable_string(variable, ret);
66✔
318
        if (r < 0 && r != -ENOENT)
66✔
319
                return log_warning_errno(r, "Failed to read EFI variable '%s', ignoring: %m", variable);
×
320

321
        return r;
322
}
323

324
static int efi_get_variable_path_and_warn(const char *variable, char **ret) {
12✔
325
        int r;
12✔
326

327
        r = efi_get_variable_path(variable, ret);
12✔
328
        if (r < 0 && r != -ENOENT)
12✔
329
                return log_warning_errno(r, "Failed to read EFI variable '%s', ignoring: %m", variable);
×
330

331
        return r;
332
}
333

334
static void print_yes_no_line(bool first, bool good, const char *name) {
186✔
335
        printf("%s%s %s\n",
546✔
336
               first ? "     Features: " : "               ",
337
               COLOR_MARK_BOOL(good),
186✔
338
               name);
339
}
186✔
340

341
int verb_status(int argc, char *argv[], void *userdata) {
33✔
342
        sd_id128_t esp_uuid = SD_ID128_NULL, xbootldr_uuid = SD_ID128_NULL;
33✔
343
        dev_t esp_devid = 0, xbootldr_devid = 0;
33✔
344
        int r, k;
33✔
345

346
        r = acquire_esp(/* unprivileged_mode= */ -1,
33✔
347
                        /* graceful= */ false,
348
                        /* ret_part= */ NULL,
349
                        /* ret_pstart= */ NULL,
350
                        /* ret_psize= */ NULL,
351
                        &esp_uuid,
352
                        &esp_devid);
353
        if (arg_print_esp_path) {
33✔
354
                if (r == -EACCES) /* If we couldn't acquire the ESP path, log about access errors (which is the only
10✔
355
                                   * error the find_esp_and_warn() won't log on its own) */
356
                        return log_error_errno(r, "Failed to determine ESP location: %m");
33✔
357
                if (r < 0)
10✔
358
                        return r;
359

360
                puts(arg_esp_path);
8✔
361
                return 0;
8✔
362
        }
363

364
        r = acquire_xbootldr(
23✔
365
                        /* unprivileged_mode= */ -1,
366
                        &xbootldr_uuid,
367
                        &xbootldr_devid);
368
        if (arg_print_dollar_boot_path) {
23✔
369
                if (r == -EACCES)
8✔
370
                        return log_error_errno(r, "Failed to determine XBOOTLDR partition: %m");
×
371
                if (r < 0)
8✔
372
                        return r;
373

374
                const char *path = arg_dollar_boot_path();
7✔
375
                if (!path)
3✔
376
                        return log_error_errno(SYNTHETIC_ERRNO(EACCES), "Failed to determine XBOOTLDR location.");
1✔
377

378
                puts(path);
6✔
379
                return 0;
6✔
380
        }
381

382
        r = 0; /* If we couldn't determine the path, then don't consider that a problem from here on, just
15✔
383
                * show what we can show */
384

385
        pager_open(arg_pager_flags);
15✔
386

387
        if (arg_root)
15✔
388
                log_debug("Skipping 'System' section, operating offline.");
6✔
389
        else if (!is_efi_boot())
9✔
390
                printf("%sSystem:%s\n"
6✔
391
                       "Not booted with EFI\n\n",
392
                       ansi_underline(), ansi_normal());
393
        else {
394
                static const struct {
6✔
395
                        uint64_t flag;
396
                        const char *name;
397
                } loader_flags[] = {
398
                        { EFI_LOADER_FEATURE_BOOT_COUNTING,           "Boot counting"                         },
399
                        { EFI_LOADER_FEATURE_CONFIG_TIMEOUT,          "Menu timeout control"                  },
400
                        { EFI_LOADER_FEATURE_CONFIG_TIMEOUT_ONE_SHOT, "One-shot menu timeout control"         },
401
                        { EFI_LOADER_FEATURE_ENTRY_DEFAULT,           "Default entry control"                 },
402
                        { EFI_LOADER_FEATURE_ENTRY_ONESHOT,           "One-shot entry control"                },
403
                        { EFI_LOADER_FEATURE_XBOOTLDR,                "Support for XBOOTLDR partition"        },
404
                        { EFI_LOADER_FEATURE_RANDOM_SEED,             "Support for passing random seed to OS" },
405
                        { EFI_LOADER_FEATURE_LOAD_DRIVER,             "Load drop-in drivers"                  },
406
                        { EFI_LOADER_FEATURE_SORT_KEY,                "Support Type #1 sort-key field"        },
407
                        { EFI_LOADER_FEATURE_SAVED_ENTRY,             "Support @saved pseudo-entry"           },
408
                        { EFI_LOADER_FEATURE_DEVICETREE,              "Support Type #1 devicetree field"      },
409
                        { EFI_LOADER_FEATURE_SECUREBOOT_ENROLL,       "Enroll SecureBoot keys"                },
410
                        { EFI_LOADER_FEATURE_RETAIN_SHIM,             "Retain SHIM protocols"                 },
411
                        { EFI_LOADER_FEATURE_MENU_DISABLE,            "Menu can be disabled"                  },
412
                        { EFI_LOADER_FEATURE_MULTI_PROFILE_UKI,       "Multi-Profile UKIs are supported"      },
413
                        { EFI_LOADER_FEATURE_REPORT_URL,              "Loader reports network boot URL"       },
414
                        { EFI_LOADER_FEATURE_TYPE1_UKI,               "Support Type #1 uki field"             },
415
                        { EFI_LOADER_FEATURE_TYPE1_UKI_URL,           "Support Type #1 uki-url field"         },
416
                        { EFI_LOADER_FEATURE_TPM2_ACTIVE_PCR_BANKS,   "Loader reports TPM2 active PCR banks"  },
417
                };
418
                static const struct {
6✔
419
                        uint64_t flag;
420
                        const char *name;
421
                } stub_flags[] = {
422
                        { EFI_STUB_FEATURE_REPORT_BOOT_PARTITION,     "Stub reports loader partition information"                   },
423
                        { EFI_STUB_FEATURE_REPORT_STUB_PARTITION,     "Stub reports stub partition information"                     },
424
                        { EFI_STUB_FEATURE_REPORT_URL,                "Stub reports network boot URL"                               },
425
                        { EFI_STUB_FEATURE_PICK_UP_CREDENTIALS,       "Picks up credentials from boot partition"                    },
426
                        { EFI_STUB_FEATURE_PICK_UP_SYSEXTS,           "Picks up system extension images from boot partition"        },
427
                        { EFI_STUB_FEATURE_PICK_UP_CONFEXTS,          "Picks up configuration extension images from boot partition" },
428
                        { EFI_STUB_FEATURE_THREE_PCRS,                "Measures kernel+command line+sysexts"                        },
429
                        { EFI_STUB_FEATURE_RANDOM_SEED,               "Support for passing random seed to OS"                       },
430
                        { EFI_STUB_FEATURE_CMDLINE_ADDONS,            "Pick up .cmdline from addons"                                },
431
                        { EFI_STUB_FEATURE_CMDLINE_SMBIOS,            "Pick up .cmdline from SMBIOS Type 11"                        },
432
                        { EFI_STUB_FEATURE_DEVICETREE_ADDONS,         "Pick up .dtb from addons"                                    },
433
                        { EFI_STUB_FEATURE_MULTI_PROFILE_UKI,         "Stub understands profile selector"                           },
434
                };
435
                _cleanup_free_ char *fw_type = NULL, *fw_info = NULL, *loader = NULL, *loader_path = NULL, *stub = NULL, *stub_path = NULL,
×
436
                        *current_entry = NULL, *oneshot_entry = NULL, *default_entry = NULL, *sysfail_entry = NULL, *sysfail_reason = NULL;
6✔
437
                uint64_t loader_features = 0, stub_features = 0;
6✔
438
                int have;
6✔
439

440
                (void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("LoaderFirmwareType"), &fw_type);
6✔
441
                (void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("LoaderFirmwareInfo"), &fw_info);
6✔
442
                (void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("LoaderInfo"), &loader);
6✔
443
                (void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("StubInfo"), &stub);
6✔
444
                (void) efi_get_variable_path_and_warn(EFI_LOADER_VARIABLE_STR("LoaderImageIdentifier"), &loader_path);
6✔
445
                (void) efi_get_variable_path_and_warn(EFI_LOADER_VARIABLE_STR("StubImageIdentifier"), &stub_path);
6✔
446
                (void) efi_loader_get_features(&loader_features);
6✔
447
                (void) efi_stub_get_features(&stub_features);
6✔
448
                (void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("LoaderEntrySelected"), &current_entry);
6✔
449
                (void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("LoaderEntryOneShot"), &oneshot_entry);
6✔
450
                (void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("LoaderEntryDefault"), &default_entry);
6✔
451
                (void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("LoaderEntrySysFail"), &sysfail_entry);
6✔
452
                (void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("LoaderSysFailReason"), &sysfail_reason);
6✔
453

454
                SecureBootMode secure = efi_get_secure_boot_mode();
6✔
455
                printf("%sSystem:%s\n", ansi_underline(), ansi_normal());
12✔
456
                printf("      Firmware: %s%s (%s)%s\n", ansi_highlight(), strna(fw_type), strna(fw_info), ansi_normal());
18✔
457
                printf(" Firmware Arch: %s\n", get_efi_arch());
6✔
458
                printf("   Secure Boot: %s%s%s",
12✔
459
                       IN_SET(secure, SECURE_BOOT_USER, SECURE_BOOT_DEPLOYED) ? ansi_highlight_green() : ansi_normal(),
6✔
460
                       enabled_disabled(IN_SET(secure, SECURE_BOOT_USER, SECURE_BOOT_DEPLOYED)),
6✔
461
                       ansi_normal());
462

463
                if (secure != SECURE_BOOT_DISABLED)
6✔
464
                        printf(" (%s)\n", secure_boot_mode_to_string(secure));
6✔
465
                else
466
                        printf("\n");
×
467

468
                Tpm2Support s = tpm2_support_full(TPM2_SUPPORT_FIRMWARE|TPM2_SUPPORT_DRIVER);
6✔
469
                printf("  TPM2 Support: %s%s%s\n",
18✔
470
                       FLAGS_SET(s, TPM2_SUPPORT_FIRMWARE|TPM2_SUPPORT_DRIVER) ? ansi_highlight_green() :
471
                       (s & (TPM2_SUPPORT_FIRMWARE|TPM2_SUPPORT_DRIVER)) != 0 ? ansi_highlight_red() : ansi_highlight_yellow(),
×
472
                       FLAGS_SET(s, TPM2_SUPPORT_FIRMWARE|TPM2_SUPPORT_DRIVER) ? "yes" :
6✔
473
                       (s & TPM2_SUPPORT_FIRMWARE) ? "firmware only, driver unavailable" :
×
474
                       (s & TPM2_SUPPORT_DRIVER) ? "driver only, firmware unavailable" : "no",
×
475
                       ansi_normal());
476

477
                k = efi_measured_uki(LOG_DEBUG);
6✔
478
                if (k > 0)
6✔
479
                        printf("  Measured UKI: %syes%s\n", ansi_highlight_green(), ansi_normal());
12✔
480
                else if (k == 0)
×
481
                        printf("  Measured UKI: no\n");
×
482
                else {
483
                        errno = -k;
×
484
                        printf("  Measured UKI: %sfailed%s (%m)\n", ansi_highlight_red(), ansi_normal());
×
485
                }
486

487
                k = efi_get_reboot_to_firmware();
6✔
488
                if (k > 0)
6✔
489
                        printf("  Boot into FW: %sactive%s\n", ansi_highlight_yellow(), ansi_normal());
×
490
                else if (k == 0)
6✔
491
                        printf("  Boot into FW: supported\n");
6✔
492
                else if (k == -EOPNOTSUPP)
×
493
                        printf("  Boot into FW: not supported\n");
×
494
                else {
495
                        errno = -k;
×
496
                        printf("  Boot into FW: %sfailed%s (%m)\n", ansi_highlight_red(), ansi_normal());
×
497
                }
498
                printf("\n");
6✔
499

500
                if (loader) {
6✔
501
                        printf("%sCurrent Boot Loader:%s\n", ansi_underline(), ansi_normal());
12✔
502
                        printf("       Product: %s%s%s\n", ansi_highlight(), loader, ansi_normal());
18✔
503
                        for (size_t i = 0; i < ELEMENTSOF(loader_flags); i++)
120✔
504
                                print_yes_no_line(i == 0, FLAGS_SET(loader_features, loader_flags[i].flag), loader_flags[i].name);
114✔
505

506
                        sd_id128_t loader_partition_uuid = SD_ID128_NULL;
6✔
507
                        (void) efi_loader_get_device_part_uuid(&loader_partition_uuid);
6✔
508

509
                        _cleanup_free_ char *loader_url = NULL;
6✔
510
                        (void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("LoaderDeviceURL"), &loader_url);
6✔
511

512
                        if (!sd_id128_is_null(loader_partition_uuid)) {
6✔
513
                                /* If we know esp_uuid and loader_partition_uuid is not equal to it, print a warning. */
514
                                if (!sd_id128_is_null(esp_uuid) && !sd_id128_equal(loader_partition_uuid, esp_uuid))
12✔
515
                                        printf("WARNING: The boot loader reports a different partition UUID than the detected ESP "
×
516
                                               "("SD_ID128_UUID_FORMAT_STR" vs. "SD_ID128_UUID_FORMAT_STR")!\n",
517
                                               SD_ID128_FORMAT_VAL(loader_partition_uuid),
×
518
                                               SD_ID128_FORMAT_VAL(esp_uuid));
×
519

520
                                printf("     Partition: /dev/disk/by-partuuid/" SD_ID128_UUID_FORMAT_STR "\n",
6✔
521
                                       SD_ID128_FORMAT_VAL(loader_partition_uuid));
6✔
522
                        } else if (loader_path)
×
523
                                printf("     Partition: n/a\n");
×
524

525
                        if (loader_path)
6✔
526
                                printf("        Loader: %s%s%s/%s%s\n",
12✔
527
                                       glyph(GLYPH_TREE_RIGHT), ansi_grey(), arg_esp_path, ansi_normal(), loader_path);
528

529
                        if (loader_url)
6✔
530
                                printf("  Net Boot URL: %s\n", loader_url);
×
531

532
                        if (sysfail_entry)
6✔
533
                                printf("SysFail Reason: %s\n", sysfail_reason);
×
534

535
                        if (current_entry)
6✔
536
                                printf(" Current Entry: %s\n", current_entry);
6✔
537
                        if (default_entry)
6✔
538
                                printf(" Default Entry: %s\n", default_entry);
3✔
539
                        if (oneshot_entry && !streq_ptr(oneshot_entry, default_entry))
6✔
540
                                printf(" OneShot Entry: %s\n", oneshot_entry);
×
541
                        if (sysfail_entry)
6✔
542
                                printf(" SysFail Entry: %s\n", sysfail_entry);
×
543

544
                        printf("\n");
6✔
545
                }
546

547
                if (stub) {
6✔
548
                        printf("%sCurrent Stub:%s\n", ansi_underline(), ansi_normal());
12✔
549
                        printf("      Product: %s%s%s\n", ansi_highlight(), stub, ansi_normal());
18✔
550
                        for (size_t i = 0; i < ELEMENTSOF(stub_flags); i++)
78✔
551
                                print_yes_no_line(i == 0, FLAGS_SET(stub_features, stub_flags[i].flag), stub_flags[i].name);
72✔
552

553
                        sd_id128_t stub_partition_uuid = SD_ID128_NULL;
6✔
554
                        (void) efi_stub_get_device_part_uuid(&stub_partition_uuid);
6✔
555

556
                        _cleanup_free_ char *stub_url = NULL;
6✔
557
                        (void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("StubDeviceURL"), &stub_url);
6✔
558

559
                        if (!sd_id128_is_null(stub_partition_uuid)) {
6✔
560
                                /* _If_ we know both esp_uuid and xbootldr_uuid and stub_partition_uuid is not equal
561
                                 * to _either_ of them, print a warning. */
562
                                if (!sd_id128_is_null(esp_uuid) && !sd_id128_equal(stub_partition_uuid, esp_uuid) &&
12✔
563
                                    !sd_id128_is_null(xbootldr_uuid) && !sd_id128_equal(stub_partition_uuid, xbootldr_uuid))
×
564
                                        printf("WARNING: The stub loader reports a different UUID than the detected ESP and XBOOTDLR partitions "
×
565
                                               "("SD_ID128_UUID_FORMAT_STR" vs. "SD_ID128_UUID_FORMAT_STR"/"SD_ID128_UUID_FORMAT_STR")!\n",
566
                                               SD_ID128_FORMAT_VAL(stub_partition_uuid),
×
567
                                               SD_ID128_FORMAT_VAL(esp_uuid),
×
568
                                               SD_ID128_FORMAT_VAL(xbootldr_uuid));
×
569

570
                                printf("    Partition: /dev/disk/by-partuuid/" SD_ID128_UUID_FORMAT_STR "\n",
6✔
571
                                       SD_ID128_FORMAT_VAL(stub_partition_uuid));
6✔
572
                        } else if (stub_path)
×
573
                                printf("    Partition: n/a\n");
×
574

575
                        if (stub_path)
6✔
576
                                printf("         Stub: %s%s\n", glyph(GLYPH_TREE_RIGHT), strna(stub_path));
6✔
577

578
                        if (stub_url)
6✔
579
                                printf(" Net Boot URL: %s\n", stub_url);
×
580

581
                        printf("\n");
6✔
582
                }
583

584
                printf("%sRandom Seed:%s\n", ansi_underline(), ansi_normal());
12✔
585
                have = access(EFIVAR_PATH(EFI_LOADER_VARIABLE_STR("LoaderSystemToken")), F_OK) >= 0;
6✔
586
                printf(" System Token: %s\n", have ? "set" : "not set");
6✔
587

588
                if (arg_esp_path) {
6✔
589
                        _cleanup_free_ char *p = NULL;
6✔
590

591
                        p = path_join(arg_esp_path, "/loader/random-seed");
6✔
592
                        if (!p)
6✔
593
                                return log_oom();
×
594

595
                        r = access(p, F_OK);
6✔
596
                        if (r < 0 && errno != ENOENT)
6✔
597
                                printf("       Exists: Can't access %s (%m)\n", p);
×
598
                        else
599
                                printf("       Exists: %s\n", yes_no(r >= 0));
6✔
600
                }
601

602
                printf("\n");
6✔
603
        }
604

605
        if (arg_esp_path)
15✔
606
                RET_GATHER(r, status_binaries(arg_esp_path, esp_uuid));
15✔
607

608
        if (!arg_root && is_efi_boot())
15✔
609
                RET_GATHER(r, status_variables());
6✔
610

611
        if (arg_esp_path || arg_xbootldr_path) {
15✔
612
                _cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL;
15✔
613

614
                k = boot_config_load_and_select(&config,
15✔
615
                                                arg_esp_path, esp_devid,
616
                                                arg_xbootldr_path, xbootldr_devid);
617
                RET_GATHER(r, k);
15✔
618

619
                if (k >= 0)
×
620
                        RET_GATHER(r,
15✔
621
                                   status_entries(&config,
622
                                                  arg_esp_path, esp_uuid,
623
                                                  arg_xbootldr_path, xbootldr_uuid));
624
        }
625

626
        return r;
627
}
628

629
static int ref_file(Hashmap **known_files, const char *fn, int increment) {
×
630
        char *k = NULL;
×
631
        int n, r;
×
632

633
        assert(known_files);
×
634

635
        /* just gracefully ignore this. This way the caller doesn't have to verify whether the bootloader
636
         * entry is relevant. */
637
        if (!fn)
×
638
                return 0;
×
639

640
        n = PTR_TO_INT(hashmap_get2(*known_files, fn, (void**)&k));
×
641
        n += increment;
×
642

643
        assert(n >= 0);
×
644

645
        if (n == 0) {
×
646
                (void) hashmap_remove(*known_files, fn);
×
647
                free(k);
×
648
        } else if (!k) {
×
649
                _cleanup_free_ char *t = NULL;
×
650

651
                t = strdup(fn);
×
652
                if (!t)
×
653
                        return -ENOMEM;
654
                r = hashmap_ensure_put(known_files, &path_hash_ops_free, t, INT_TO_PTR(n));
×
655
                if (r < 0)
×
656
                        return r;
657
                TAKE_PTR(t);
658
        } else {
659
                r = hashmap_update(*known_files, fn, INT_TO_PTR(n));
×
660
                if (r < 0)
×
661
                        return r;
×
662
        }
663

664
        return n;
665
}
666

667
static void deref_unlink_file(Hashmap **known_files, const char *fn, const char *root) {
×
668
        _cleanup_free_ char *path = NULL;
×
669
        int r;
×
670

671
        assert(known_files);
×
672

673
        /* just gracefully ignore this. This way the caller doesn't
674
           have to verify whether the bootloader entry is relevant */
675
        if (!fn || !root)
×
676
                return;
677

678
        r = ref_file(known_files, fn, -1);
×
679
        if (r < 0)
×
680
                return (void) log_warning_errno(r, "Failed to deref \"%s\", ignoring: %m", fn);
×
681
        if (r > 0)
×
682
                return;
683

684
        if (arg_dry_run) {
×
685
                r = chase_and_access(fn, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, F_OK, &path);
×
686
                if (r < 0)
×
687
                        log_info_errno(r, "Unable to determine whether \"%s\" exists, ignoring: %m", fn);
×
688
                else
689
                        log_info("Would remove \"%s\"", path);
×
690
                return;
×
691
        }
692

693
        r = chase_and_unlink(fn, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, 0, &path);
×
694
        if (r >= 0)
×
695
                log_info("Removed \"%s\"", path);
×
696
        else if (r != -ENOENT)
×
697
                return (void) log_warning_errno(r, "Failed to remove \"%s\", ignoring: %m", fn);
×
698

699
        _cleanup_free_ char *d = NULL;
×
700
        if (path_extract_directory(fn, &d) >= 0 && !path_equal(d, "/")) {
×
701
                r = chase_and_unlink(d, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, AT_REMOVEDIR, NULL);
×
702
                if (r < 0 && !IN_SET(r, -ENOTEMPTY, -ENOENT))
×
703
                        log_warning_errno(r, "Failed to remove directory \"%s\", ignoring: %m", d);
×
704
        }
705
}
706

707
static int count_known_files(const BootConfig *config, const char* root, Hashmap **ret_known_files) {
×
708
        _cleanup_hashmap_free_ Hashmap *known_files = NULL;
×
709
        int r;
×
710

711
        assert(config);
×
712
        assert(ret_known_files);
×
713

714
        for (size_t i = 0; i < config->n_entries; i++) {
×
715
                const BootEntry *e = config->entries + i;
×
716

717
                if (!path_equal(e->root, root))
×
718
                        continue;
×
719

720
                r = ref_file(&known_files, e->kernel, +1);
×
721
                if (r < 0)
×
722
                        return r;
723
                r = ref_file(&known_files, e->efi, +1);
×
724
                if (r < 0)
×
725
                        return r;
726
                STRV_FOREACH(s, e->initrd) {
×
727
                        r = ref_file(&known_files, *s, +1);
×
728
                        if (r < 0)
×
729
                                return r;
730
                }
731
                r = ref_file(&known_files, e->device_tree, +1);
×
732
                if (r < 0)
×
733
                        return r;
734
                STRV_FOREACH(s, e->device_tree_overlay) {
×
735
                        r = ref_file(&known_files, *s, +1);
×
736
                        if (r < 0)
×
737
                                return r;
738
                }
739
        }
740

741
        *ret_known_files = TAKE_PTR(known_files);
×
742

743
        return 0;
×
744
}
745

746
static int boot_config_find_in(const BootConfig *config, const char *root, const char *id) {
×
747
        assert(config);
×
748

749
        if (!root || !id)
×
750
                return -ENOENT;
751

752
        for (size_t i = 0; i < config->n_entries; i++)
×
753
                if (path_equal(config->entries[i].root, root) &&
×
754
                    fnmatch(id, config->entries[i].id, FNM_CASEFOLD) == 0)
×
755
                        return i;
×
756

757
        return -ENOENT;
758
}
759

760
static int unlink_entry(const BootConfig *config, const char *root, const char *id) {
×
761
        _cleanup_hashmap_free_ Hashmap *known_files = NULL;
×
762
        const BootEntry *e = NULL;
×
763
        int r;
×
764

765
        assert(config);
×
766

767
        r = count_known_files(config, root, &known_files);
×
768
        if (r < 0)
×
769
                return log_error_errno(r, "Failed to count files in %s: %m", root);
×
770

771
        r = boot_config_find_in(config, root, id);
×
772
        if (r < 0)
×
773
                return r;
774

775
        if (r == config->default_entry)
×
776
                log_warning("%s is the default boot entry", id);
×
777
        if (r == config->selected_entry)
×
778
                log_warning("%s is the selected boot entry", id);
×
779

780
        e = &config->entries[r];
×
781

782
        deref_unlink_file(&known_files, e->kernel, e->root);
×
783
        deref_unlink_file(&known_files, e->efi, e->root);
×
784
        STRV_FOREACH(s, e->initrd)
×
785
                deref_unlink_file(&known_files, *s, e->root);
×
786
        deref_unlink_file(&known_files, e->device_tree, e->root);
×
787
        STRV_FOREACH(s, e->device_tree_overlay)
×
788
                deref_unlink_file(&known_files, *s, e->root);
×
789

790
        if (arg_dry_run)
×
791
                log_info("Would remove \"%s\"", e->path);
×
792
        else {
793
                r = chase_and_unlink(e->path, root, CHASE_PROHIBIT_SYMLINKS, 0, NULL);
×
794
                if (r < 0)
×
795
                        return log_error_errno(r, "Failed to remove \"%s\": %m", e->path);
×
796

797
                log_info("Removed %s", e->path);
×
798
        }
799

800
        return 0;
801
}
802

803
static int list_remove_orphaned_file(
×
804
                RecurseDirEvent event,
805
                const char *path,
806
                int dir_fd,
807
                int inode_fd,
808
                const struct dirent *de,
809
                const struct statx *sx,
810
                void *userdata) {
811

812
        Hashmap *known_files = userdata;
×
813

814
        assert(path);
×
815

816
        if (event != RECURSE_DIR_ENTRY)
×
817
                return RECURSE_DIR_CONTINUE;
818

819
        if (hashmap_get(known_files, path))
×
820
                return RECURSE_DIR_CONTINUE; /* keep! */
821

822
        if (arg_dry_run)
×
823
                log_info("Would remove %s", path);
×
824
        else if (unlinkat(dir_fd, de->d_name, 0) < 0)
×
825
                log_warning_errno(errno, "Failed to remove \"%s\", ignoring: %m", path);
×
826
        else
827
                log_info("Removed %s", path);
×
828

829
        return RECURSE_DIR_CONTINUE;
830
}
831

832
static int cleanup_orphaned_files(
×
833
                const BootConfig *config,
834
                const char *root) {
835

836
        _cleanup_hashmap_free_ Hashmap *known_files = NULL;
×
837
        _cleanup_free_ char *full = NULL, *p = NULL;
×
838
        _cleanup_close_ int dir_fd = -EBADF;
×
839
        int r;
×
840

841
        assert(config);
×
842
        assert(root);
×
843

844
        log_info("Cleaning %s", root);
×
845

846
        r = settle_entry_token();
×
847
        if (r < 0)
×
848
                return r;
849

850
        r = count_known_files(config, root, &known_files);
×
851
        if (r < 0)
×
852
                return log_error_errno(r, "Failed to count files in %s: %m", root);
×
853

854
        dir_fd = chase_and_open(arg_entry_token, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS,
×
855
                        O_DIRECTORY|O_CLOEXEC, &full);
856
        if (dir_fd == -ENOENT)
×
857
                return 0;
858
        if (dir_fd < 0)
×
859
                return log_error_errno(dir_fd, "Failed to open '%s/%s': %m", root, skip_leading_slash(arg_entry_token));
×
860

861
        p = path_join("/", arg_entry_token);
×
862
        if (!p)
×
863
                return log_oom();
×
864

865
        r = recurse_dir(dir_fd, p, 0, UINT_MAX, RECURSE_DIR_SORT, list_remove_orphaned_file, known_files);
×
866
        if (r < 0)
×
867
                return log_error_errno(r, "Failed to cleanup %s: %m", full);
×
868

869
        return r;
870
}
871

872
int verb_list(int argc, char *argv[], void *userdata) {
12✔
873
        _cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL;
12✔
874
        dev_t esp_devid = 0, xbootldr_devid = 0;
12✔
875
        int r;
12✔
876

877
        /* If we lack privileges we invoke find_esp_and_warn() in "unprivileged mode" here, which does two
878
         * things: turn off logging about access errors and turn off potentially privileged device probing.
879
         * Here we're interested in the latter but not the former, hence request the mode, and log about
880
         * EACCES. */
881

882
        r = acquire_esp(/* unprivileged_mode= */ -1, /* graceful= */ false, NULL, NULL, NULL, NULL, &esp_devid);
12✔
883
        if (r == -EACCES) /* We really need the ESP path for this call, hence also log about access errors */
12✔
884
                return log_error_errno(r, "Failed to determine ESP location: %m");
×
885
        if (r < 0)
12✔
886
                return r;
887

888
        r = acquire_xbootldr(/* unprivileged_mode= */ -1, NULL, &xbootldr_devid);
12✔
889
        if (r == -EACCES)
12✔
890
                return log_error_errno(r, "Failed to determine XBOOTLDR partition: %m");
×
891
        if (r < 0)
12✔
892
                return r;
893

894
        r = boot_config_load_and_select(&config, arg_esp_path, esp_devid, arg_xbootldr_path, xbootldr_devid);
12✔
895
        if (r < 0)
12✔
896
                return r;
897

898
        if (config.n_entries == 0 && !sd_json_format_enabled(arg_json_format_flags)) {
12✔
899
                log_info("No boot loader entries found.");
4✔
900
                return 0;
4✔
901
        }
902

903
        if (streq(argv[0], "list")) {
8✔
904
                pager_open(arg_pager_flags);
8✔
905
                return show_boot_entries(&config, arg_json_format_flags);
8✔
906
        } else if (streq(argv[0], "cleanup")) {
×
907
                if (arg_xbootldr_path && xbootldr_devid != esp_devid)
×
908
                        cleanup_orphaned_files(&config, arg_xbootldr_path);
×
909
                return cleanup_orphaned_files(&config, arg_esp_path);
×
910
        } else {
911
                assert(streq(argv[0], "unlink"));
×
912
                if (arg_xbootldr_path && xbootldr_devid != esp_devid) {
×
913
                        r = unlink_entry(&config, arg_xbootldr_path, argv[1]);
×
914
                        if (r == 0 || r != -ENOENT)
×
915
                                return r;
916
                }
917
                return unlink_entry(&config, arg_esp_path, argv[1]);
×
918
        }
919
}
920

921
int verb_unlink(int argc, char *argv[], void *userdata) {
×
922
        return verb_list(argc, argv, userdata);
×
923
}
924

925
int vl_method_list_boot_entries(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
1✔
926
        _cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL;
×
927
        dev_t esp_devid = 0, xbootldr_devid = 0;
1✔
928
        int r;
1✔
929

930
        assert(link);
1✔
931

932
        r = sd_varlink_dispatch(link, parameters, /* dispatch_table = */ NULL, /* userdata = */ NULL);
1✔
933
        if (r != 0)
1✔
934
                return r;
935

936
        if (!FLAGS_SET(flags, SD_VARLINK_METHOD_MORE))
1✔
937
                return sd_varlink_error(link, SD_VARLINK_ERROR_EXPECTED_MORE, NULL);
×
938

939
        r = acquire_esp(/* unprivileged_mode= */ false,
1✔
940
                        /* graceful= */ false,
941
                        /* ret_part= */ NULL,
942
                        /* ret_pstart= */ NULL,
943
                        /* ret_psize= */ NULL,
944
                        /* ret_uuid=*/ NULL,
945
                        &esp_devid);
946
        if (r == -EACCES) /* We really need the ESP path for this call, hence also log about access errors */
1✔
947
                return log_error_errno(r, "Failed to determine ESP location: %m");
×
948
        if (r < 0)
1✔
949
                return r;
950

951
        r = acquire_xbootldr(
1✔
952
                        /* unprivileged_mode= */ false,
953
                        /* ret_uuid= */ NULL,
954
                        &xbootldr_devid);
955
        if (r == -EACCES)
1✔
956
                return log_error_errno(r, "Failed to determine XBOOTLDR partition: %m");
×
957
        if (r < 0)
1✔
958
                return r;
959

960
        r = boot_config_load_and_select(&config, arg_esp_path, esp_devid, arg_xbootldr_path, xbootldr_devid);
1✔
961
        if (r < 0)
1✔
962
                return r;
963

964
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *previous = NULL;
1✔
965
        for (size_t i = 0; i < config.n_entries; i++) {
4✔
966
                if (previous) {
3✔
967
                        r = sd_varlink_notifybo(link, SD_JSON_BUILD_PAIR_VARIANT("entry", previous));
2✔
968
                        if (r < 0)
2✔
969
                                return r;
970

971
                        previous = sd_json_variant_unref(previous);
2✔
972
                }
973

974
                r = boot_entry_to_json(&config, i, &previous);
3✔
975
                if (r < 0)
3✔
976
                        return r;
977
        }
978

979
        if (!previous)
1✔
980
                return sd_varlink_error(link, "io.systemd.BootControl.NoSuchBootEntry", NULL);
×
981

982
        return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_VARIANT("entry", previous));
1✔
983
}
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