This commit is contained in:
Ward Nakchbandi (Cosmic Fusion) 2023-08-06 23:03:36 +03:00
parent 99cda286f2
commit 5294c5eabc
250 changed files with 11933 additions and 2 deletions

2
debian/changelog vendored
View File

@ -1,4 +1,4 @@
refind-btrfs (0.6.0-99pika1.lunar) lunar; urgency=medium
refind-btrfs (0.6.0-99pika2.lunar) lunar; urgency=medium
* Initial Creation

View File

@ -1,5 +1,6 @@
# Clone Upstream
git clone https://github.com/Venom1991/refind-btrfs -b v0.6.0
#git clone https://github.com/Venom1991/refind-btrfs -b v0.6.0
cp -rvf ./refind-btrfs.install ./debian/
cp -rvf ./debian ./refind-btrfs/
cd ./refind-btrfs

View File

@ -0,0 +1,215 @@
#######################
## refind-btrfs.conf ##
#######################
# TOML syntax
# esp_uuid = <string>
## Explicitly defined ESP's Part-UUID which can be used in case the ESP itself
## cannot be automatically located on the system (for whatever reason).
## This option is, by default, defined as an empty UUID which means that it is
## ignored.
esp_uuid = "00000000-0000-0000-0000-000000000000"
# exit_if_root_is_snapshot = <bool>
## Whether to issue a warning and prematurely exit in case the root partition
## is already mounted as a snapshot.
## WARNING: Disabling this option is considered experimental and may result in
## unstable and/or erroneous behavior.
exit_if_root_is_snapshot = true
# exit_if_no_changes_are_detected = <bool>
## Whether to issue a warning and prematurely exit in case no changes were
## detected by comparing the preparation results of the current run with those
## of the previous run (if it exists).
## Changes are considered to be detected in case any of the following
## conditions are satisfied:
## • this configuration file was modified
## • rEFInd's configuration file (main or included) was modified
## • at least one snapshot was found (either for addition or removal)
## The time of last modification (st_mtime) is used to detect file changes
## instead of comparing their contents.
exit_if_no_changes_are_detected = true
# [[snapshot-search]]
## Array of objects used to configure the behavior of searching for snapshots.
## The directory (or directories) listed in this array (including nested
## directories, up to "max_depth" - 1) are also watched for changes by the
## background running mode.
#
# directory = <string>
## Directory in which to search for snapshots (absolute filesystem path).
## WARNING: This directory must not be the same as or nested in the directory
## defined by the "destination_dir" option (shown further below).
#
# is_nested = <bool>
## Whether to search for snapshots nested within another snapshot. Only one
## level of nesting is supported and a search is performed in the same
## directory (if it exists) that is, in this context, relative to the found
## snapshot's root directory instead of the system's root directory. The same
## maximum search depth is used, as well.
## Setting this option to "false" potentially also means stopping the search
## prematurely (i.e., before the maximum search depth was ever reached) in
## those branches in which a snapshot was found.
#
# max_depth = <int>
## Maximum search depth relative to the search directory.
## WARNING: Defining a large value can seriously impact performance (of both
## searching for snapshots and watching for directory changes) in case the tree
## (whose root is the search directory) is sufficiently large (deep and/or
## wide).
[[snapshot-search]]
directory = "/.snapshots"
is_nested = false
max_depth = 2
# [snapshot-manipulation]
## Object used to configure the behavior of preparatory steps required
## to enable booting into snapshots as well as deleting those that aren't
## needed anymore.
#
# selection_count = <int> or <string>
## Number of snapshots (sorted descending by creation time) to include or
## "inf" to always include every currently present snapshot.
#
# modify_read_only_flag = <bool>
## Whether to change the read-only flag of a snapshot instead of creating
## a new writable snapshot from it. This option has no meaning for those
## snapshots that are already writable.
#
# destination_directory = <string>
## Directory in which writable snapshots are to be placed (absolute filesystem
## path). This option has no meaning in case the "modify_read_only_flag" option
## is set to "true". It needn't exist beforehand as it is created in case it
## doesn't (including its missing parents, if any).
## WARNING: This directory must not be the same as or nested in an ony of the
## snapshot search directories.
#
# cleanup_exclusion = <array<string>>
## Array comprised of UUIDs (duplicates are ignored) of previously
## created writable snapshots that are to be excluded during automatic cleanup.
## These snapshots will not be deleted and should always appear as part of a
## generated boot stanza.
## See the output of "btrfs subvolume show <snapshot-filesystem-path>" for
## the expected format (shown in the "UUID" column). Same remark applies here
## with regards to the "modify_read_only_flag" option.
[snapshot-manipulation]
selection_count = 5
modify_read_only_flag = false
destination_directory = "/root/.refind-btrfs"
cleanup_exclusion = []
# [boot-stanza-generation]
## Object used to configure the process of combining the source boot stanza
## with previously prepared snapshots into a generated boot stanza.
#
# refind_config = <string>
## Name of rEFInd's main configuration file which must reside somewhere on
## the ESP. This option must not be defined as a path (neither absolute nor
## relative).
#
# include_paths = <bool>
## Whether to adjust the "loader" and "initrd" paths found in the source boot
## stanza. Setting this option to "true" while having a separate /boot
## partition has no meaning and is ignored.
#
# include_sub_menus = <bool>
## Whether to include sub-menus ("submenuentry") defined as part of the source
## boot stanza in the generated boot stanza. If set to "true", only those
## sub-menus which do not override the main stanza's "loader" and "options"
## fields and which do not delete (i.e., set it to nothing) its "initrd" field
## are taken into consideration.
## WARNING: Enabling this option in combination with setting a large
## "selection_count" value (greater than 10, for example) or, worse yet, by
## setting it to "inf" can potentially result in an overcrowded "Boot Options"
## menu.
#
## source_exclusion = <array<string>>
## Array comprised of loader filenames ("loader") with which the matched source
## boot stanzas can be arbitrarily excluded from processing, i.e., these boot
## stanzas will not be taken into account during the generation phase.
## For example, it can be defined as: ["vmlinuz-linux", "vmlinuz-linux-lts"].
## WARNING: This array must not contain all of the matched source boot stanza's
## loader filenames. If it does, an error is issued and a premature exit is
## performed.
## Also, a manual cleanup of the generated boot stanza (or stanzas) and its
## inclusion within the rEFInd's main configuration file is required in case
## the array's members were defined after the fact.
[boot-stanza-generation]
refind_config = "refind.conf"
include_paths = true
include_sub_menus = false
source_exclusion = []
# [boot-stanza-generation.icon]
## Subobject used to configure the process of defining the generated boot
## stanza's icon.
#
# mode = <string>
## Selected mode of icon generation which can be defined as one of:
## • "default" - the source boot stanza's icon is reused, as is
## • "custom" - a user provided image file path is used as the icon
## • "embed_btrfs_logo" - the Btrfs logo is embedded into the source boot
## stanza's icon
#
## path = <string>
## Path of the user provided image file, relative to whichever directory the
## file defined by the "refind_config" option was found. This option is taken
## into consideration in case the "mode" option is set to "custom" but is
## otherwise ignored.
## WARNING: The format of the image located at this path must be one of those
## which rEFInd itself supports, that is one of the following: PNG, JPEG, BMP
## or ICNS.
[boot-stanza-generation.icon]
mode = "default"
path = "btrfs-snapshot-stanzas/icons/sample_icon.png"
# [boot-stanza-generation.icon.btrfs-logo]
## Subobject used to configure the behavior of embedding the Btrfs logo into
## the source boot stanza's icon. It is taken into consideration in case the
## "mode" option is set to "embed_btrfs_logo" but is otherwise ignored.
## WARNING: The source boot stanza icon's format must be PNG and its dimensions
## (width or height) must exceed those defined by the "size" option.
#
# variant = <string>
## Btrfs logo variant to be used for embedding which can be defined as one of:
## • "original" - dark variant, suitable for light themes (including the
## default theme)
## • "inverted" - light variant created by inverting the original logo's
## pixels' color values, suitable for dark themes
#
# size = <string>
## Size of the chosen Btrfs logo's variant defined by the "type" option. Both
## variants come in three different sizes which should be a sufficiently
## flexible choice and as such suitable for a decent number of different OS
## icons, both default and custom. It can be defined as one of:
## • "small" - 32x20 pixels
## • "medium" - 48x30 pixels
## • "large" - 64x40 pixels
#
# horizontal_alignment = <string>
## Horizontal alignment (x-axis) of the embedded Btrfs logo which can be
## defined as one of:
## • "left"
## • "center"
## • "right"
#
# vertical_alignment = <string>
## Vertical alignment (y-axis) of the embedded Btrfs logo which can be defined
## as one of:
## • "top"
## • "center"
## • "bottom"
[boot-stanza-generation.icon.btrfs-logo]
variant = "original"
size = "medium"
horizontal_alignment = "center"
vertical_alignment = "center"

View File

@ -0,0 +1,2 @@
#!/bin/bash
python -m refind_btrfs --run-mode one-time

View File

@ -0,0 +1,273 @@
Metadata-Version: 2.1
Name: refind-btrfs
Version: 0.6.0
Summary: Generate rEFInd manual boot stanzas from Btrfs snapshots
Home-page: https://github.com/Venom1991/refind-btrfs
Author: Luka Žaja
Author-email: luka.zaja@protonmail.com
Maintainer: Luka Žaja
Maintainer-email: luka.zaja@protonmail.com
License: GNU General Public License v3 or later (GPLv3+)
Keywords: rEFInd,btrfs
Platform: Linux
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Natural Language :: English
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: System :: Boot
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Provides-Extra: custom_icon
License-File: LICENSE.txt
# refind-btrfs
### Table of Contents
- [Description](#description)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Configuration](#configuration)
- [Example](#example)
- [Implementation](#implementation)
- [Further Efforts](#further-efforts)
## Description
This tool is used to automate a few tedious tasks required to boot into [Btrfs](https://en.wikipedia.org/wiki/Btrfs) snapshots from [rEFInd](https://en.wikipedia.org/wiki/REFInd). It is to rEFInd what [grub-btrfs](https://github.com/Antynea/grub-btrfs) is to [GRUB](https://en.wikipedia.org/wiki/GNU_GRUB).
What it does is the following:
* Gathers information about block devices present in the system
* Identifies the [ESP](https://en.wikipedia.org/wiki/EFI_system_partition) (either by GPT GUID or MBR ID)
* Gathers information about mounted filesystems (from [mtab](https://en.wikipedia.org/wiki/Mtab)) which are present on all of the found block devices
* Identifies the root mount point and gathers information about the subvolume which is mounted at said mount point
* Searches for snapshots of the identified subvolume in the configured directory (or directories)
* Searches for rEFInd's main config file on the ESP and parses it to extract [manual boot stanzas](https://www.rodsbooks.com/refind/configfile.html#stanzas) from it (included configs are also analyzed, if present)
* Selects the configured number of latest snapshots and uses them as such if they are writable and if any aren't, it either (depending on the configuration):
* sets their read-only flag to false, thus making them writable
* creates new writable snapshots from them in the configured location
* Aligns the root mount point in the [fstab](https://en.wikipedia.org/wiki/Fstab) file of each selected snapshot with the snapshot itself
* Deletes outdated previously created writable snapshots (if any exist)
* Generates new manual boot stanzas from identified ones where every relevant field is aligned with each selected snapshot
* Finally, it saves the generated manual boot stanzas in separate config files (outputs them to a subdirectory) and includes each file in the main config file so as not to needlessly clutter it
In case a separate /boot partition is detected only the fields relevant to / are modified ("subvol" and/or "subvolid") while the "loader" and "initrd" fields (the former may also be nested within the "options" field) remain unaffected.
It goes without saying that the consequence of having this kind of a setup is being unable to mitigate a problematic kernel upgrade by simply booting into a snapshot.
This tool will also detect a situation where / is mounted as a snapshot (which means that you've already booted into one), issue a warning and simply exit whereas, for instance, [Snapper](http://snapper.io/) will happily continue creating its snapshots, regardless. This behavior is configurable and enabled by default.
## Prerequisites
The following conditions (some are probably superfluous at this point) must be satisfied in order for this tool to function correctly:
* mounted ESP (no automatic discovery and/or mounting is supported)
* Btrfs formatted filesystem with a subvolume mounted as /
* at least one snapshot of the root subvolume
* rEFInd installation present on the ESP
* at least one manual boot stanza (found in rEFInd's main config file or in any of the additional config files included within it) defined such that (see the [ArchWiki](https://wiki.archlinux.org/index.php/REFInd#Manual_boot_stanza) for an example) its own "options" field or any such field belonging to at least one of its sub-menus contains definitions of the following boot loader options:
* the "root" option must be matched with the root partition (by PARTUUID or PARTLABEL), its filesystem (by UUID or LABEL) or with a block device (by name) which itself represents the root partition
* the "rootflags" option must define a "subvol" suboption which is matched with the root subvolume's logical path and/or a "subvolid" suboption which is matched with the root subvolume's ID
## Installation
This tool is currently available only in the [AUR](https://aur.archlinux.org/packages/refind-btrfs/) which means that [Arch Linux](https://www.archlinux.org/) users (as well as users of derivative distributions, I imagine) can easily install it.
It comes with a script (refind-btrfs) which can be used to perform the described steps, on-demand (root privileges are required to run it). There is also a [systemd](https://en.wikipedia.org/wiki/Systemd) service aptly named **refind-btrfs.service** which runs the tool in a background mode of operation where the described steps are performed automatically once a change (snapshot creation or deletion) happens in the watched snapshot directories which are the same ones as those in which it searches for snapshots. If you are using Snapper along with its capability to take regular snapshots on boot this service should take these into account as well because it is set to start before Snapper's relevant service does so (the one named snapper-boot.service).
Before running the script for the first time or enabling and starting the service make sure to at least check and perhaps modify the config file (/etc/refind-btrfs.conf) to suit your own needs.
If you wish to check the current status and log output of the running service you can do so by executing:
```
systemctl status refind-btrfs
journalctl -u refind-btrfs -b
```
Alternatively, there exists a [PyPI](https://pypi.org/project/refind-btrfs/) package but bear in mind that since [libbtrfsutil](https://github.com/kdave/btrfs-progs/tree/master/libbtrfsutil) isn't available on PyPI it needs to be already present in the system site packages (its Python bindings, to be precise) because it cannot be automatically pulled in as a dependency. Chances are that it is available for your distribution of choice (search for a package named "btrfs-progs") but you most probably already have it installed as I suppose you are using Btrfs, after all.
Also, every file contained in [this](https://github.com/Venom1991/refind-btrfs/tree/master/src/refind_btrfs/data) directory should be copied to the following locations:
* refind-btrfs script to /usr/bin (or wherever it is you keep your system-wide executables)
* refind-btrfs.conf-sample as refind-btrfs.conf (without the "-sample" suffix) to /etc
* refind-btrfs.service to /usr/lib/systemd/system (if you are using systemd and wish to utilize the snapshot directory watching feature)
In case the custom generated boot stanza's icon feature (explained in the next section) is desired it can initially be enabled by installing this package with the following command:
```
pip install refind-btrfs[custom_icon]
```
You should also create an empty directory named "refind-btrfs" in /var/lib as the tool expects that it is present. Additionally, if you wish to be able to use the Btrfs logo embedding mode of custom icon generation you should also copy the "[icons](https://github.com/Venom1991/refind-btrfs/tree/master/src/refind_btrfs/data/icons)" directory into the previously created one.
## Configuration
Every option is thoroughly explained in the sample config [file](https://github.com/Venom1991/refind-btrfs/blob/master/src/refind_btrfs/data/refind-btrfs.conf-sample).
In case you've opted to use the provided systemd service and wish to change the search directories (in this context, these are actually watched directories) in the config file while it is running you must restart it manually after doing so because the directory observer is started only once and an automatic restart is not performed.
The default configuration is meant to enable seamless integration with Snapper simply because I'm using it but the tool itself doesn't depend on it and ought to function with different setups. Also, by default the tool is configured for creating new writable snapshots intended for booting instead of in-place modification of the found snapshots' read-only flags as I believe this is the safer (or perhaps even saner) choice.
[Timeshift](https://github.com/teejee2008/timeshift) users can try setting the [default](https://github.com/Venom1991/refind-btrfs/blob/d1e3c474ed88d7b1ad3948d75bf6f167da676c5d/src/refind_btrfs/data/refind-btrfs.conf-sample#L65) snapshot search directory to "/run/timeshift/backup/timeshift-btrfs/snapshots" and the corresponding maximum search depth to three.
If you're having trouble with the ESP being automatically located, the "esp_uuid" option could prove to be useful. If an actual UUID is provided (not the default, empty one), this value will be used to compare partition UUIDs (returned by lsblk) instead of comparing their types with hardcoded GPT UUID or MBR ID values.
Custom generated boot stanza icon support is also implemented, by default the source boot stanza's icon is reused. It is possible to provide one's own custom icon's path or to embed the Btrfs logo (comes in two variants and three sizes per each variant) into the source boot stanza's icon instead. This combined icon is then used as the generated boot stanza's icon.
In order for these two additional modes of operation (not the default one) to work an optional dependency has to be installed - namely, the [Pillow](https://python-pillow.org) library which can be installed from the official Arch Linux [repository](https://archlinux.org/packages/community/x86_64/python-pillow/) or from [PyPI](https://pypi.org/project/Pillow/).
It is imperative that you don't just blindly try to boot into a given snapshot (simply because no errors were reported) before verifying the generated manual boot stanza, either by inspecting the file contents in which it was saved or by viewing the boot loader [options](https://www.rodsbooks.com/refind/using.html#boot_options) using rEFInd and also not before verifying the chosen snapshot's fstab file.
## Example
Given a setup such as this one:
* device /dev/nvme0n1 where:
* the ESP is on /dev/nvme0n1p3 mounted at /efi
* / is on /dev/nvme0n1p8
* /boot is included in /dev/nvme0n1p8 (**not** a separate partition)
* the subvolume mounted as / is named @
* fstab file's root mount point:
```
UUID=95250e8a-5870-45df-a7b3-3b3ee8873c16 / btrfs rw,noatime,compress-force=zstd:2,ssd,space_cache=v2,commit=15,subvolid=256,subvol=/@ 0 0
```
* manual boot stanza defined in the refind.conf file (rEFInd's main config file, in this case):
```
menuentry "Arch Linux - Stable" {
icon /EFI/refind/icons/os_arch.png
volume ARCH
loader /@/boot/vmlinuz-linux
initrd /@/boot/initramfs-linux.img
options "root=PARTUUID=048d6fcd-c88c-504d-bd51-dfc0a5bf762d rw add_efi_memmap rootflags=subvol=@ initrd=@\boot\intel-ucode.img"
submenuentry "Boot - fallback" {
initrd /@/boot/initramfs-linux-fallback.img
}
submenuentry "Boot - terminal" {
add_options "systemd.unit=multi-user.target"
}
}
```
* five read-only snapshots located in the /.snapshots directory where this directory is itself mounted as a subvolume named @snapper-root (this last bit isn't particularly relevant):
| Absolute Path | Time of Creation | Subvolume ID |
| ---------------------- | ------------------- | ------------ |
| /.snapshots/1/snapshot | 10-12-2020 01:00:00 | 498 |
| /.snapshots/2/snapshot | 11-12-2020 02:00:00 | 499 |
| /.snapshots/3/snapshot | 12-12-2020 03:00:00 | 500 |
| /.snapshots/4/snapshot | 13-12-2020 04:00:00 | 501 |
| /.snapshots/5/snapshot | 14-12-2020 05:00:00 | 502 |
* refind-btrfs.conf file changed such that the "selection_count" option is set to 3 instead of the default 5
When run, this tool should select the latest three snapshots (3, 4 and 5 from the list) and create new, writable ones from these in the directory configured by the "destination_dir" option where each snapshot is named by formatting the time of creation ("YYYY-mm-dd_HH-MM-SS") of the snapshot it was created from, adding a "rwsnap" prefix to it and also adding the original snapshot's subvolume ID as a suffix. In the rare case when different snapshots have identical timestamps their monotonic numerical IDs are there to ensure uniqueness.
Afterwards, the resultant snapshots' generated names should look like this:
* rwsnap_2020-12-12_03-00-00_ID500,
* rwsnap_2020-12-13_04-00-00_ID501 and
* rwsnap_2020-12-14_05-00-00_ID502
This naming scheme makes sense to me because when choosing a snapshot to boot from you most probably want to know when the original snapshot was created and not the one created from it because the time delay depends on when this tool was run and, if sufficiently large, can completely mislead you. If you've chosen to use the systemd service this delay shouldn't be significant (measuring a mere few seconds at worst, ideally).
The most recent snapshot's fstab file should (after being modified) contain a root mount point which looks like this:
```
UUID=95250e8a-5870-45df-a7b3-3b3ee8873c16 / btrfs rw,noatime,compress-force=zstd:2,ssd,space_cache=v2,commit=15,subvolid=503,subvol=/@/root/.refind-btrfs/rwsnap_2020-12-14_05-00-00_ID502 0 0
```
I'm assuming here that the next available subvolume ID was 503 (an increment of one) which implies that the writable snapshot was created immediately after the original snapshot was taken but that doesn't necessarily have to be the case and its specific value doesn't ultimately matter that much as long as it directly corresponds to the newly created snapshot which it absolutely should (otherwise, mounting it as / would fail due to the mismatch).
With this setup the newly created snapshot ended up being nested under the root subvolume but you can of course make your own adjustments as you see fit. This tool will only create the destination directory in case it doesn't exist. It won't do anything other than that.
I've personally created another subvolume named @rw-snapshots directly under the default filesystem subvolume (ID 5) and mounted it at /root/.refind-btrfs. In my case the logical path of rwsnap_2020-12-14_05-00-00_ID502 would be /@rw-snapshots/rwsnap_2020-12-14_05-00-00_ID502.
A generated manual boot stanza's filename is formatted like "{volume}_{loader}.conf" and converted to all lowercase letters which would result in, for this example, a file named "arch_vmlinuz-linux.conf". This file is then saved in a subdirectory (relative to rEFInd's root directory) named "btrfs-snapshot-stanzas" and finally included in the main config file by appending an "include" directive which would, again for this example, look like this: "include btrfs-snapshot-stanzas/arch_vmlinuz-linux.conf". This last step is performed only once, during an initial run. Afterwards, it is detected as already being included in the main config file.
You are free to rearrange the appended include directives however you want, this tool does not care about where exactly they appear in the main config file. This is particularly useful in case you've defined multiple boot stanzas (each one pointing to a different kernel image, for example) and wish to alter the order of the boot menu entries.
The generated file's contents (representing the generated stanza) should look like this:
```
menuentry "Arch Linux - Stable (rwsnap_2020-12-14_05-00-00_ID502)" {
icon /EFI/refind/icons/os_arch.png
volume ARCH
loader /@/root/.refind-btrfs/rwsnap_2020-12-14_05-00-00_ID502/boot/vmlinuz-linux
initrd /@/root/.refind-btrfs/rwsnap_2020-12-14_05-00-00_ID502/boot/initramfs-linux.img
options "root=PARTUUID=048d6fcd-c88c-504d-bd51-dfc0a5bf762d rw add_efi_memmap rootflags=subvol=@/root/.refind-btrfs/rwsnap_2020-12-14_05-00-00_ID502 initrd=@\root\.refind-btrfs\rwsnap_2020-12-14_05-00-00_ID502\boot\intel-ucode.img"
submenuentry "Arch Linux - Stable (rwsnap_2020-12-13_04-00-00_ID501)" {
loader /@/root/.refind-btrfs/rwsnap_2020-12-13_04-00-00_ID501/boot/vmlinuz-linux
initrd /@/root/.refind-btrfs/rwsnap_2020-12-13_04-00-00_ID501/boot/initramfs-linux.img
options "root=PARTUUID=048d6fcd-c88c-504d-bd51-dfc0a5bf762d rw add_efi_memmap rootflags=subvol=@/root/.refind-btrfs/rwsnap_2020-12-13_04-00-00_ID501 initrd=@\root\.refind-btrfs\rwsnap_2020-12-13_04-00-00_ID501\boot\intel-ucode.img"
}
submenuentry "Arch Linux - Stable (rwsnap_2020-12-12_03-00-00_ID500)" {
loader /@/root/.refind-btrfs/rwsnap_2020-12-12_03-00-00_ID500/boot/vmlinuz-linux
initrd /@/root/.refind-btrfs/rwsnap_2020-12-12_03-00-00_ID500/boot/initramfs-linux.img
options "root=PARTUUID=048d6fcd-c88c-504d-bd51-dfc0a5bf762d rw add_efi_memmap rootflags=subvol=@/root/.refind-btrfs/rwsnap_2020-12-12_03-00-00_ID500 initrd=@\root\.refind-btrfs\rwsnap_2020-12-12_03-00-00_ID500\boot\intel-ucode.img"
}
}
```
As you've probably noticed, this tool leverages rEFInd's overriding features, that is to say "submenuentry" sections are used to incorporate successive snapshots into the stanza itself by overriding the "loader", "initrd" and "options" fields of the main boot stanza which itself represents the latest snapshot.
If you've configured this tool to also take into account the original boot stanza's sub-menus the resultant generated boot stanza should look like this:
```
menuentry "Arch Linux - Stable (rwsnap_2020-12-14_05-00-00_ID502)" {
icon /EFI/refind/icons/os_arch.png
volume ARCH
loader /@/root/.refind-btrfs/rwsnap_2020-12-14_05-00-00_ID502/boot/vmlinuz-linux
initrd /@/root/.refind-btrfs/rwsnap_2020-12-14_05-00-00_ID502/boot/initramfs-linux.img
options "root=PARTUUID=048d6fcd-c88c-504d-bd51-dfc0a5bf762d rw add_efi_memmap rootflags=subvol=@/root/.refind-btrfs/rwsnap_2020-12-14_05-00-00_ID502 initrd=@\root\.refind-btrfs\rwsnap_2020-12-14_05-00-00_ID502\boot\intel-ucode.img"
submenuentry "Boot - fallback (rwsnap_2020-12-14_05-00-00_ID502)" {
initrd /@/root/.refind-btrfs/rwsnap_2020-12-14_05-00-00_ID502/boot/initramfs-linux-fallback.img
}
submenuentry "Boot - terminal (rwsnap_2020-12-14_05-00-00_ID502)" {
add_options "systemd.unit=multi-user.target"
}
submenuentry "Arch Linux - Stable (rwsnap_2020-12-13_04-00-00_ID501)" {
loader /@/root/.refind-btrfs/rwsnap_2020-12-13_04-00-00_ID501/boot/vmlinuz-linux
initrd /@/root/.refind-btrfs/rwsnap_2020-12-13_04-00-00_ID501/boot/initramfs-linux.img
options "root=PARTUUID=048d6fcd-c88c-504d-bd51-dfc0a5bf762d rw add_efi_memmap rootflags=subvol=@/root/.refind-btrfs/rwsnap_2020-12-13_04-00-00_ID501 initrd=@\root\.refind-btrfs\rwsnap_2020-12-13_04-00-00_ID501\boot\intel-ucode.img"
}
submenuentry "Boot - fallback (rwsnap_2020-12-13_04-00-00_ID501)" {
loader /@/root/.refind-btrfs/rwsnap_2020-12-13_04-00-00_ID501/boot/vmlinuz-linux
initrd /@/root/.refind-btrfs/rwsnap_2020-12-13_04-00-00_ID501/boot/initramfs-linux-fallback.img
options "root=PARTUUID=048d6fcd-c88c-504d-bd51-dfc0a5bf762d rw add_efi_memmap rootflags=subvol=@/root/.refind-btrfs/rwsnap_2020-12-13_04-00-00_ID501 initrd=@\root\.refind-btrfs\rwsnap_2020-12-13_04-00-00_ID01\boot\intel-ucode.img"
}
submenuentry "Boot - terminal (rwsnap_2020-12-13_04-00-00_ID501)" {
loader /@/root/.refind-btrfs/rwsnap_2020-12-13_04-00-00_ID501/boot/vmlinuz-linux
initrd /@/root/.refind-btrfs/rwsnap_2020-12-13_04-00-00_ID501/boot/initramfs-linux.img
options "root=PARTUUID=048d6fcd-c88c-504d-bd51-dfc0a5bf762d rw add_efi_memmap rootflags=subvol=@/root/.refind-btrfs/rwsnap_2020-12-13_04-00-00_ID501 initrd=@\root\.refind-btrfs\rwsnap_2020-12-13_04-00-00_ID501\boot\intel-ucode.img systemd.unit=multi-user.target"
}
submenuentry "Arch Linux - Stable (rwsnap_2020-12-12_03-00-00_ID500)" {
loader /@/root/.refind-btrfs/rwsnap_2020-12-12_03-00-00_ID500/boot/vmlinuz-linux
initrd /@/root/.refind-btrfs/rwsnap_2020-12-12_03-00-00_ID500/boot/initramfs-linux.img
options "root=PARTUUID=048d6fcd-c88c-504d-bd51-dfc0a5bf762d rw add_efi_memmap rootflags=subvol=@/root/.refind-btrfs/rwsnap_2020-12-12_03-00-00_ID500 initrd=@\root\.refind-btrfs\rwsnap_2020-12-12_03-00-00_ID500\boot\intel-ucode.img"
}
submenuentry "Boot - fallback (rwsnap_2020-12-12_03-00-00_ID500)" {
loader /@/root/.refind-btrfs/rwsnap_2020-12-12_03-00-00_ID500/boot/vmlinuz-linux
initrd /@/root/.refind-btrfs/rwsnap_2020-12-12_03-00-00_ID500/boot/initramfs-linux-fallback.img
options "root=PARTUUID=048d6fcd-c88c-504d-bd51-dfc0a5bf762d rw add_efi_memmap rootflags=subvol=@/root/.refind-btrfs/rwsnap_2020-12-12_03-00-00_ID500 initrd=@\root\.refind-btrfs\rwsnap_2020-12-12_03-00-00_ID500\boot\intel-ucode.img"
}
submenuentry "Boot - terminal (rwsnap_2020-12-12_03-00-00_ID500)" {
loader /@/root/.refind-btrfs/rwsnap_2020-12-12_03-00-00_ID500/boot/vmlinuz-linux
initrd /@/root/.refind-btrfs/rwsnap_2020-12-12_03-00-00_ID500/boot/initramfs-linux.img
options "root=PARTUUID=048d6fcd-c88c-504d-bd51-dfc0a5bf762d rw add_efi_memmap rootflags=subvol=@/root/.refind-btrfs/rwsnap_2020-12-12_03-00-00_ID500 initrd=@\root\.refind-btrfs\rwsnap_2020-12-12_03-00-00_ID500\boot\intel-ucode.img systemd.unit=multi-user.target"
}
}
```
A couple of notable details are the fact that the "add_options" field (if it exists) of any given sub-menu belonging to a successive snapshot is merged with the "options" field of the corresponding snapshot's sub-menu and also the fact that the latest snapshot's sub-menus implicitly inherit those main stanza's fields which they themselves do not override in the original boot stanza. Consequently, these sub-menus' definitions are intentionally similar to those of their counterparts found in the original boot stanza.
This is how an Arch Linux installation with three different kernels (XanMod, Stable and LTS) should appear in rEFInd (the default theme is shown) after this tool has successfully completed its job:
![rEFInd Screenshot Default](src/refind_btrfs/data/images/refind_screenshot_default.png)
Here, each manual boot stanza uses its own custom icon based on the default Arch Linux OS icon. The Btrfs logo is then also embedded into these icons (by setting [this](https://github.com/Venom1991/refind-btrfs/blob/4e0e629680fc581143c684e6e90958cbe26db8fc/src/refind_btrfs/data/refind-btrfs.conf-sample#L158) option to "embed_btrfs_logo") and the resultant icons are defined as part of their corresponding generated boot stanzas.
By using a darker theme (such as the [Nord](https://github.com/jaltuna/refind-theme-nord) theme - shown in the following screenshot) and by using the "inverted" Btrfs logo's variant (as opposed to the "original" one, shown in the previous screenshot), the same Arch Linux installation should appear in rEFInd looking like this:
![rEFInd Screenshot Nord](src/refind_btrfs/data/images/refind_screenshot_nord.png)
## Implementation
Most relevant dependencies:
* block device and ESP information is gathered using [lsblk](https://man7.org/linux/man-pages/man8/lsblk.8.html) (supports JSON output)
* mtab information is gathered using [findmnt](https://man7.org/linux/man-pages/man8/findmnt.8.html) (same remark applies regarding the output)
* all of the mentioned subvolume and snapshot operations are performed using [libbtrfsutil](https://github.com/kdave/btrfs-progs/tree/master/libbtrfsutil)
* [ANLTR4](https://github.com/antlr/antlr4) was used to generate the lexer and parser required for rEFInd config files' analyses
* [Watchdog](https://github.com/gorakhargosh/watchdog) is used for the snapshot directory watching feature and is utilized in a non-recursive fashion (watches all of the configured search directories as well as directories nested under these, up to configured maximum depth reduced by one)
* [python-systemd](https://github.com/systemd/python-systemd) is used for notifying systemd about the service's readiness (because its type is set to "notify") and also for logging to the journal
[Shelve](https://docs.python.org/3/library/shelve.html) is used to keep track of the currently processed snapshots and also to avoid analyzing the rEFInd config file each time as it is quite an expensive task. A new analysis is performed in case the current and actual times of modification differ ([st_mtime](https://docs.python.org/3/library/os.html#os.stat_result.st_mtime) is used for that purpose) which means that simply touching the file should also trigger a new analysis (file hashes aren't computed nor consequently compared). This fact also explains the need for a directory in /var/lib as the database file resides in it.
The directory watching mechanism is a bit unfortunate in a sense that it is way overkill for the task at hand. Even though Watchdog is a great, battle-tested library and many people use it, I feel that this solution isn't particularly well suited to this tool but it will simply have to suffice for now as I don't have a better idea (grub-btrfs also relies on a similar mechanism), at least not until the Btrfs authors develop [this](https://btrfs.wiki.kernel.org/index.php/Project_ideas#Send_notifications_about_important_events) useful feature or something akin to it.
## Further Efforts
Currently, this tool won't clean up after itself in case, for instance, creating writable snapshots succeeds but generating a manual boot stanza from them fails (for whatever reason). The correct thing to do would be to delete these snapshots altogether (thus undoing the changes made by the previous step or roll-backing as it is often called) meaning that the whole run is considered to be successful if and only if all of the steps it performed were successful.
This behavior would then be comparable with the [atomicity](https://en.wikipedia.org/wiki/Atomicity_(database_systems)) principle to which most database systems adhere. The previously mentioned scenario is covered in a different way by issuing a relevant warning on the next attempt to run the tool (because the writable snapshots already exist at this point in time and they aren't expected to) but also continuing to perform successive steps. This isn't a general solution, of course, but more of a workaround for this one possible scenario.
With that said, being somehow able to preview changes proposed by this tool would also be beneficial, especially after altering its configuration.
A more elaborate snapshot selection mechanism would be appreciated, comparable to what Snapper does, that is selecting a configurable number of daily, weekly, etc. snapshots to be included in the generated manual boot stanza.
Generated boot stanzas' names are initialized using a hardcoded format string which is not ideal. It would be more convenient to provide a way for users to define their own format string using a combination of predefined variables (time of the source snapshot's creation, its numerical ID, etc.) along with some entirely arbitrary parts.
But, before trying to implement any of these shiny features this project's source code should be properly documented and tests should be written for it because, presently, there aren't any. The latter is also a pretty considerable effort due to the sheer number of different test cases. Luckily, all of the external dependencies (OS commands, third-party library calls and similar) are abstracted away which means that no significant preparatory steps regarding the codebase to be tested are required beforehand.

View File

@ -0,0 +1,97 @@
LICENSE.txt
MANIFEST.in
README.md
pyproject.toml
setup.cfg
setup.py
src/refind_btrfs/__init__.py
src/refind_btrfs/__main__.py
src/refind_btrfs.egg-info/PKG-INFO
src/refind_btrfs.egg-info/SOURCES.txt
src/refind_btrfs.egg-info/dependency_links.txt
src/refind_btrfs.egg-info/entry_points.txt
src/refind_btrfs.egg-info/requires.txt
src/refind_btrfs.egg-info/top_level.txt
src/refind_btrfs/boot/__init__.py
src/refind_btrfs/boot/boot_options.py
src/refind_btrfs/boot/boot_stanza.py
src/refind_btrfs/boot/file_refind_config_provider.py
src/refind_btrfs/boot/refind_config.py
src/refind_btrfs/boot/refind_listeners.py
src/refind_btrfs/boot/refind_visitors.py
src/refind_btrfs/boot/sub_menu.py
src/refind_btrfs/boot/antlr4/RefindConfigLexer.py
src/refind_btrfs/boot/antlr4/RefindConfigParser.py
src/refind_btrfs/boot/antlr4/RefindConfigParserVisitor.py
src/refind_btrfs/boot/antlr4/__init__.py
src/refind_btrfs/boot/migrations/__init__.py
src/refind_btrfs/boot/migrations/icon_migration_strategies.py
src/refind_btrfs/boot/migrations/main_migration_strategies.py
src/refind_btrfs/boot/migrations/migration.py
src/refind_btrfs/boot/migrations/state.py
src/refind_btrfs/common/__init__.py
src/refind_btrfs/common/boot_files_check_result.py
src/refind_btrfs/common/checkable_observer.py
src/refind_btrfs/common/configurable_mixin.py
src/refind_btrfs/common/constants.py
src/refind_btrfs/common/enums.py
src/refind_btrfs/common/exceptions.py
src/refind_btrfs/common/package_config.py
src/refind_btrfs/common/abc/__init__.py
src/refind_btrfs/common/abc/base_config.py
src/refind_btrfs/common/abc/base_runner.py
src/refind_btrfs/common/abc/commands/__init__.py
src/refind_btrfs/common/abc/commands/device_command.py
src/refind_btrfs/common/abc/commands/icon_command.py
src/refind_btrfs/common/abc/commands/subvolume_command.py
src/refind_btrfs/common/abc/factories/__init__.py
src/refind_btrfs/common/abc/factories/base_device_command_factory.py
src/refind_btrfs/common/abc/factories/base_icon_command_factory.py
src/refind_btrfs/common/abc/factories/base_logger_factory.py
src/refind_btrfs/common/abc/factories/base_subvolume_command_factory.py
src/refind_btrfs/common/abc/providers/__init__.py
src/refind_btrfs/common/abc/providers/base_package_config_provider.py
src/refind_btrfs/common/abc/providers/base_persistence_provider.py
src/refind_btrfs/common/abc/providers/base_refind_config_provider.py
src/refind_btrfs/console/__init__.py
src/refind_btrfs/console/cli_runner.py
src/refind_btrfs/data/refind-btrfs
src/refind_btrfs/data/refind-btrfs.conf-sample
src/refind_btrfs/data/refind-btrfs.service
src/refind_btrfs/data/icons/btrfs_logo/inverted_large.png
src/refind_btrfs/data/icons/btrfs_logo/inverted_medium.png
src/refind_btrfs/data/icons/btrfs_logo/inverted_small.png
src/refind_btrfs/data/icons/btrfs_logo/original_large.png
src/refind_btrfs/data/icons/btrfs_logo/original_medium.png
src/refind_btrfs/data/icons/btrfs_logo/original_small.png
src/refind_btrfs/data/images/refind_screenshot_default.png
src/refind_btrfs/data/images/refind_screenshot_nord.png
src/refind_btrfs/device/__init__.py
src/refind_btrfs/device/block_device.py
src/refind_btrfs/device/filesystem.py
src/refind_btrfs/device/mount_options.py
src/refind_btrfs/device/partition.py
src/refind_btrfs/device/partition_table.py
src/refind_btrfs/device/subvolume.py
src/refind_btrfs/service/__init__.py
src/refind_btrfs/service/snapshot_event_handler.py
src/refind_btrfs/service/snapshot_observer.py
src/refind_btrfs/service/watchdog_runner.py
src/refind_btrfs/state_management/__init__.py
src/refind_btrfs/state_management/conditions.py
src/refind_btrfs/state_management/model.py
src/refind_btrfs/state_management/refind_btrfs_machine.py
src/refind_btrfs/system/__init__.py
src/refind_btrfs/system/btrfsutil_command.py
src/refind_btrfs/system/command_factories.py
src/refind_btrfs/system/findmnt_command.py
src/refind_btrfs/system/fstab_command.py
src/refind_btrfs/system/lsblk_command.py
src/refind_btrfs/system/pillow_command.py
src/refind_btrfs/utility/__init__.py
src/refind_btrfs/utility/file_package_config_provider.py
src/refind_btrfs/utility/helpers.py
src/refind_btrfs/utility/injector_modules.py
src/refind_btrfs/utility/level_aware_formatter.py
src/refind_btrfs/utility/logger_factories.py
src/refind_btrfs/utility/shelve_persistence_provider.py

View File

@ -0,0 +1,2 @@
[console_scripts]
refind-btrfs = refind_btrfs:main

View File

@ -0,0 +1,13 @@
antlr4-python3-runtime
injector
more-itertools
pid
semantic-version
systemd-python
tomlkit
transitions
typeguard
watchdog
[custom_icon]
Pillow

View File

@ -0,0 +1,91 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
import os
from argparse import ArgumentParser
from typing import Optional
from injector import Injector
from refind_btrfs.common import constants
from refind_btrfs.common.abc import BaseRunner
from refind_btrfs.common.abc.factories import BaseLoggerFactory
from refind_btrfs.common.enums import RunMode
from refind_btrfs.common.exceptions import PackageConfigError
from refind_btrfs.utility.helpers import check_access_rights, checked_cast, none_throws
from refind_btrfs.utility.injector_modules import CLIModule, WatchdogModule
def initialize_injector() -> Optional[Injector]:
one_time_mode = RunMode.ONE_TIME.value
background_mode = RunMode.BACKGROUND.value
parser = ArgumentParser(
prog="refind-btrfs",
usage="%(prog)s [options]",
description="Generate rEFInd manual boot stanzas from Btrfs snapshots",
)
parser.add_argument(
"-rm",
"--run-mode",
help="Mode of execution",
choices=[one_time_mode, background_mode],
type=str,
nargs="?",
const=one_time_mode,
default=one_time_mode,
)
arguments = parser.parse_args()
run_mode = checked_cast(str, none_throws(arguments.run_mode))
if run_mode == one_time_mode:
return Injector(CLIModule)
elif run_mode == background_mode:
return Injector(WatchdogModule)
return None
def main() -> int:
exit_code = os.EX_OK
injector = none_throws(initialize_injector())
logger_factory = injector.get(BaseLoggerFactory)
logger = logger_factory.logger(__name__)
try:
check_access_rights()
runner = injector.get(BaseRunner)
exit_code = runner.run()
except PackageConfigError as e:
exit_code = constants.EX_NOT_OK
logger.error(e.formatted_message)
except PermissionError as e:
exit_code = e.errno
logger.error(e.strerror)
except Exception:
exit_code = constants.EX_NOT_OK
logger.exception(constants.MESSAGE_UNEXPECTED_ERROR)
return exit_code

View File

@ -0,0 +1,29 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
import sys
from . import main
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,27 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from .boot_options import BootOptions
from .boot_stanza import BootStanza
from .refind_config import RefindConfig
from .sub_menu import SubMenu

View File

@ -0,0 +1,435 @@
# Generated from c:\Users\Luka\Projects\Python\refind-btrfs\src\refind_btrfs\boot\antlr4\RefindConfigLexer.g4 by ANTLR 4.12.0
from antlr4 import *
from io import StringIO
import sys
if sys.version_info[1] > 5:
from typing import TextIO
else:
from typing.io import TextIO
def serializedATN():
return [
4,0,23,1007,6,-1,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,
5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,
12,2,13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,
19,7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,
25,2,26,7,26,2,27,7,27,2,28,7,28,1,0,4,0,62,8,0,11,0,12,0,63,1,0,
1,0,1,1,3,1,69,8,1,1,1,1,1,1,1,1,1,1,2,3,2,76,8,2,1,2,1,2,1,2,1,
2,1,3,1,3,5,3,84,8,3,10,3,12,3,87,9,3,1,3,3,3,90,8,3,1,3,1,3,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,1,4,
1,4,1,4,1,4,1,4,1,4,1,4,3,4,789,8,4,1,4,5,4,792,8,4,10,4,12,4,795,
9,4,1,4,3,4,798,8,4,1,4,1,4,1,5,1,5,3,5,804,8,5,1,6,1,6,1,6,1,6,
1,6,1,6,1,6,1,6,1,6,1,6,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,1,7,
1,7,1,7,1,7,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,9,1,9,1,9,1,9,1,9,1,9,
1,9,1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11,
1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,13,1,13,
1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,14,1,14,
1,14,1,14,1,14,1,14,1,14,1,14,1,15,1,15,1,15,1,15,1,15,1,15,1,15,
1,15,1,15,1,15,1,15,1,15,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,
1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,17,1,17,1,17,1,17,
1,17,1,17,1,17,1,17,1,17,1,18,1,18,1,18,1,18,1,18,1,18,1,18,1,18,
1,19,1,19,1,20,1,20,1,21,4,21,938,8,21,11,21,12,21,939,1,22,1,22,
1,23,1,23,1,23,3,23,947,8,23,1,24,1,24,4,24,951,8,24,11,24,12,24,
952,1,24,1,24,1,25,1,25,4,25,959,8,25,11,25,12,25,960,1,25,1,25,
1,26,4,26,966,8,26,11,26,12,26,967,1,27,1,27,1,27,1,27,1,27,1,27,
1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,
1,27,1,27,1,27,1,27,1,27,1,27,3,27,995,8,27,1,27,1,27,1,28,1,28,
1,28,1,28,1,28,3,28,1004,8,28,1,28,1,28,0,0,29,2,1,4,2,6,3,8,4,10,
5,12,6,14,0,16,0,18,7,20,8,22,9,24,10,26,11,28,12,30,13,32,14,34,
15,36,16,38,17,40,18,42,19,44,20,46,0,48,21,50,0,52,0,54,0,56,22,
58,23,2,0,1,4,2,0,9,9,32,32,1,0,10,10,3,0,48,57,65,70,97,102,2,0,
9,10,32,32,1067,0,2,1,0,0,0,0,4,1,0,0,0,0,6,1,0,0,0,0,8,1,0,0,0,
0,10,1,0,0,0,0,12,1,0,0,0,0,18,1,0,0,0,0,20,1,0,0,0,0,22,1,0,0,0,
0,24,1,0,0,0,0,26,1,0,0,0,0,28,1,0,0,0,0,30,1,0,0,0,0,32,1,0,0,0,
0,34,1,0,0,0,0,36,1,0,0,0,0,38,1,0,0,0,0,40,1,0,0,0,0,42,1,0,0,0,
0,44,1,0,0,0,0,48,1,0,0,0,1,56,1,0,0,0,1,58,1,0,0,0,2,61,1,0,0,0,
4,68,1,0,0,0,6,75,1,0,0,0,8,81,1,0,0,0,10,788,1,0,0,0,12,803,1,0,
0,0,14,805,1,0,0,0,16,815,1,0,0,0,18,828,1,0,0,0,20,835,1,0,0,0,
22,842,1,0,0,0,24,849,1,0,0,0,26,854,1,0,0,0,28,865,1,0,0,0,30,878,
1,0,0,0,32,886,1,0,0,0,34,898,1,0,0,0,36,915,1,0,0,0,38,924,1,0,
0,0,40,932,1,0,0,0,42,934,1,0,0,0,44,937,1,0,0,0,46,941,1,0,0,0,
48,946,1,0,0,0,50,948,1,0,0,0,52,956,1,0,0,0,54,965,1,0,0,0,56,994,
1,0,0,0,58,1003,1,0,0,0,60,62,7,0,0,0,61,60,1,0,0,0,62,63,1,0,0,
0,63,61,1,0,0,0,63,64,1,0,0,0,64,65,1,0,0,0,65,66,6,0,0,0,66,3,1,
0,0,0,67,69,5,13,0,0,68,67,1,0,0,0,68,69,1,0,0,0,69,70,1,0,0,0,70,
71,5,10,0,0,71,72,1,0,0,0,72,73,6,1,0,0,73,5,1,0,0,0,74,76,3,2,0,
0,75,74,1,0,0,0,75,76,1,0,0,0,76,77,1,0,0,0,77,78,3,4,1,0,78,79,
1,0,0,0,79,80,6,2,0,0,80,7,1,0,0,0,81,85,5,35,0,0,82,84,8,1,0,0,
83,82,1,0,0,0,84,87,1,0,0,0,85,83,1,0,0,0,85,86,1,0,0,0,86,89,1,
0,0,0,87,85,1,0,0,0,88,90,3,4,1,0,89,88,1,0,0,0,89,90,1,0,0,0,90,
91,1,0,0,0,91,92,6,3,0,0,92,9,1,0,0,0,93,94,5,97,0,0,94,95,5,108,
0,0,95,96,5,115,0,0,96,97,5,111,0,0,97,98,5,95,0,0,98,99,5,115,0,
0,99,100,5,99,0,0,100,101,5,97,0,0,101,102,5,110,0,0,102,103,5,95,
0,0,103,104,5,100,0,0,104,105,5,105,0,0,105,106,5,114,0,0,106,789,
5,115,0,0,107,108,5,98,0,0,108,109,5,97,0,0,109,110,5,110,0,0,110,
111,5,110,0,0,111,112,5,101,0,0,112,789,5,114,0,0,113,114,5,98,0,
0,114,115,5,97,0,0,115,116,5,110,0,0,116,117,5,110,0,0,117,118,5,
101,0,0,118,119,5,114,0,0,119,120,5,95,0,0,120,121,5,115,0,0,121,
122,5,99,0,0,122,123,5,97,0,0,123,124,5,108,0,0,124,789,5,101,0,
0,125,126,5,98,0,0,126,127,5,105,0,0,127,128,5,103,0,0,128,129,5,
95,0,0,129,130,5,105,0,0,130,131,5,99,0,0,131,132,5,111,0,0,132,
133,5,110,0,0,133,134,5,95,0,0,134,135,5,115,0,0,135,136,5,105,0,
0,136,137,5,122,0,0,137,789,5,101,0,0,138,139,5,99,0,0,139,140,5,
115,0,0,140,141,5,114,0,0,141,142,5,95,0,0,142,143,5,118,0,0,143,
144,5,97,0,0,144,145,5,108,0,0,145,146,5,117,0,0,146,147,5,101,0,
0,147,789,5,115,0,0,148,149,5,100,0,0,149,150,5,101,0,0,150,151,
5,102,0,0,151,152,5,97,0,0,152,153,5,117,0,0,153,154,5,108,0,0,154,
155,5,116,0,0,155,156,5,95,0,0,156,157,5,115,0,0,157,158,5,101,0,
0,158,159,5,108,0,0,159,160,5,101,0,0,160,161,5,99,0,0,161,162,5,
116,0,0,162,163,5,105,0,0,163,164,5,111,0,0,164,789,5,110,0,0,165,
166,5,100,0,0,166,167,5,111,0,0,167,168,5,110,0,0,168,169,5,39,0,
0,169,170,5,116,0,0,170,171,5,95,0,0,171,172,5,115,0,0,172,173,5,
99,0,0,173,174,5,97,0,0,174,175,5,110,0,0,175,176,5,95,0,0,176,177,
5,100,0,0,177,178,5,105,0,0,178,179,5,114,0,0,179,789,5,115,0,0,
180,181,5,100,0,0,181,182,5,111,0,0,182,183,5,110,0,0,183,184,5,
39,0,0,184,185,5,116,0,0,185,186,5,95,0,0,186,187,5,115,0,0,187,
188,5,99,0,0,188,189,5,97,0,0,189,190,5,110,0,0,190,191,5,95,0,0,
191,192,5,102,0,0,192,193,5,105,0,0,193,194,5,108,0,0,194,195,5,
101,0,0,195,789,5,115,0,0,196,197,5,100,0,0,197,198,5,111,0,0,198,
199,5,110,0,0,199,200,5,39,0,0,200,201,5,116,0,0,201,202,5,95,0,
0,202,203,5,115,0,0,203,204,5,99,0,0,204,205,5,97,0,0,205,206,5,
110,0,0,206,207,5,95,0,0,207,208,5,102,0,0,208,209,5,105,0,0,209,
210,5,114,0,0,210,211,5,109,0,0,211,212,5,119,0,0,212,213,5,97,0,
0,213,214,5,114,0,0,214,789,5,101,0,0,215,216,5,100,0,0,216,217,
5,111,0,0,217,218,5,110,0,0,218,219,5,39,0,0,219,220,5,116,0,0,220,
221,5,95,0,0,221,222,5,115,0,0,222,223,5,99,0,0,223,224,5,97,0,0,
224,225,5,110,0,0,225,226,5,95,0,0,226,227,5,116,0,0,227,228,5,111,
0,0,228,229,5,111,0,0,229,230,5,108,0,0,230,789,5,115,0,0,231,232,
5,100,0,0,232,233,5,111,0,0,233,234,5,110,0,0,234,235,5,39,0,0,235,
236,5,116,0,0,236,237,5,95,0,0,237,238,5,115,0,0,238,239,5,99,0,
0,239,240,5,97,0,0,240,241,5,110,0,0,241,242,5,95,0,0,242,243,5,
118,0,0,243,244,5,111,0,0,244,245,5,108,0,0,245,246,5,117,0,0,246,
247,5,109,0,0,247,248,5,101,0,0,248,789,5,115,0,0,249,250,5,100,
0,0,250,251,5,111,0,0,251,252,5,110,0,0,252,253,5,116,0,0,253,254,
5,95,0,0,254,255,5,115,0,0,255,256,5,99,0,0,256,257,5,97,0,0,257,
258,5,110,0,0,258,259,5,95,0,0,259,260,5,100,0,0,260,261,5,105,0,
0,261,262,5,114,0,0,262,789,5,115,0,0,263,264,5,100,0,0,264,265,
5,111,0,0,265,266,5,110,0,0,266,267,5,116,0,0,267,268,5,95,0,0,268,
269,5,115,0,0,269,270,5,99,0,0,270,271,5,97,0,0,271,272,5,110,0,
0,272,273,5,95,0,0,273,274,5,102,0,0,274,275,5,105,0,0,275,276,5,
108,0,0,276,277,5,101,0,0,277,789,5,115,0,0,278,279,5,100,0,0,279,
280,5,111,0,0,280,281,5,110,0,0,281,282,5,116,0,0,282,283,5,95,0,
0,283,284,5,115,0,0,284,285,5,99,0,0,285,286,5,97,0,0,286,287,5,
110,0,0,287,288,5,95,0,0,288,289,5,102,0,0,289,290,5,105,0,0,290,
291,5,114,0,0,291,292,5,109,0,0,292,293,5,119,0,0,293,294,5,97,0,
0,294,295,5,114,0,0,295,789,5,101,0,0,296,297,5,100,0,0,297,298,
5,111,0,0,298,299,5,110,0,0,299,300,5,116,0,0,300,301,5,95,0,0,301,
302,5,115,0,0,302,303,5,99,0,0,303,304,5,97,0,0,304,305,5,110,0,
0,305,306,5,95,0,0,306,307,5,116,0,0,307,308,5,111,0,0,308,309,5,
111,0,0,309,310,5,108,0,0,310,789,5,115,0,0,311,312,5,100,0,0,312,
313,5,111,0,0,313,314,5,110,0,0,314,315,5,116,0,0,315,316,5,95,0,
0,316,317,5,115,0,0,317,318,5,99,0,0,318,319,5,97,0,0,319,320,5,
110,0,0,320,321,5,95,0,0,321,322,5,118,0,0,322,323,5,111,0,0,323,
324,5,108,0,0,324,325,5,117,0,0,325,326,5,109,0,0,326,327,5,101,
0,0,327,789,5,115,0,0,328,329,5,101,0,0,329,330,5,110,0,0,330,331,
5,97,0,0,331,332,5,98,0,0,332,333,5,108,0,0,333,334,5,101,0,0,334,
335,5,95,0,0,335,336,5,97,0,0,336,337,5,110,0,0,337,338,5,100,0,
0,338,339,5,95,0,0,339,340,5,108,0,0,340,341,5,111,0,0,341,342,5,
99,0,0,342,343,5,107,0,0,343,344,5,95,0,0,344,345,5,118,0,0,345,
346,5,109,0,0,346,789,5,120,0,0,347,348,5,101,0,0,348,349,5,110,
0,0,349,350,5,97,0,0,350,351,5,98,0,0,351,352,5,108,0,0,352,353,
5,101,0,0,353,354,5,95,0,0,354,355,5,109,0,0,355,356,5,111,0,0,356,
357,5,117,0,0,357,358,5,115,0,0,358,789,5,101,0,0,359,360,5,101,
0,0,360,361,5,110,0,0,361,362,5,97,0,0,362,363,5,98,0,0,363,364,
5,108,0,0,364,365,5,101,0,0,365,366,5,95,0,0,366,367,5,116,0,0,367,
368,5,111,0,0,368,369,5,117,0,0,369,370,5,99,0,0,370,789,5,104,0,
0,371,372,5,101,0,0,372,373,5,120,0,0,373,374,5,116,0,0,374,375,
5,114,0,0,375,376,5,97,0,0,376,377,5,95,0,0,377,378,5,107,0,0,378,
379,5,101,0,0,379,380,5,114,0,0,380,381,5,110,0,0,381,382,5,101,
0,0,382,383,5,108,0,0,383,384,5,95,0,0,384,385,5,118,0,0,385,386,
5,101,0,0,386,387,5,114,0,0,387,388,5,115,0,0,388,389,5,105,0,0,
389,390,5,111,0,0,390,391,5,110,0,0,391,392,5,95,0,0,392,393,5,115,
0,0,393,394,5,116,0,0,394,395,5,114,0,0,395,396,5,105,0,0,396,397,
5,110,0,0,397,398,5,103,0,0,398,789,5,115,0,0,399,400,5,102,0,0,
400,401,5,111,0,0,401,402,5,108,0,0,402,403,5,100,0,0,403,404,5,
95,0,0,404,405,5,108,0,0,405,406,5,105,0,0,406,407,5,110,0,0,407,
408,5,117,0,0,408,409,5,120,0,0,409,410,5,95,0,0,410,411,5,107,0,
0,411,412,5,101,0,0,412,413,5,114,0,0,413,414,5,110,0,0,414,415,
5,101,0,0,415,416,5,108,0,0,416,789,5,115,0,0,417,418,5,102,0,0,
418,419,5,111,0,0,419,420,5,108,0,0,420,421,5,108,0,0,421,422,5,
111,0,0,422,423,5,119,0,0,423,424,5,95,0,0,424,425,5,115,0,0,425,
426,5,121,0,0,426,427,5,109,0,0,427,428,5,108,0,0,428,429,5,105,
0,0,429,430,5,110,0,0,430,431,5,107,0,0,431,789,5,115,0,0,432,433,
5,102,0,0,433,434,5,111,0,0,434,435,5,110,0,0,435,789,5,116,0,0,
436,437,5,104,0,0,437,438,5,105,0,0,438,439,5,100,0,0,439,440,5,
101,0,0,440,441,5,117,0,0,441,789,5,105,0,0,442,443,5,105,0,0,443,
444,5,99,0,0,444,445,5,111,0,0,445,446,5,110,0,0,446,447,5,115,0,
0,447,448,5,95,0,0,448,449,5,100,0,0,449,450,5,105,0,0,450,789,5,
114,0,0,451,452,5,108,0,0,452,453,5,111,0,0,453,454,5,103,0,0,454,
455,5,95,0,0,455,456,5,108,0,0,456,457,5,101,0,0,457,458,5,118,0,
0,458,459,5,101,0,0,459,789,5,108,0,0,460,461,5,109,0,0,461,462,
5,97,0,0,462,463,5,120,0,0,463,464,5,95,0,0,464,465,5,116,0,0,465,
466,5,97,0,0,466,467,5,103,0,0,467,789,5,115,0,0,468,469,5,109,0,
0,469,470,5,111,0,0,470,471,5,117,0,0,471,472,5,115,0,0,472,473,
5,101,0,0,473,474,5,95,0,0,474,475,5,115,0,0,475,476,5,105,0,0,476,
477,5,122,0,0,477,789,5,101,0,0,478,479,5,109,0,0,479,480,5,111,
0,0,480,481,5,117,0,0,481,482,5,115,0,0,482,483,5,101,0,0,483,484,
5,95,0,0,484,485,5,115,0,0,485,486,5,112,0,0,486,487,5,101,0,0,487,
488,5,101,0,0,488,789,5,100,0,0,489,490,5,114,0,0,490,491,5,101,
0,0,491,492,5,115,0,0,492,493,5,111,0,0,493,494,5,108,0,0,494,495,
5,117,0,0,495,496,5,116,0,0,496,497,5,105,0,0,497,498,5,111,0,0,
498,789,5,110,0,0,499,500,5,115,0,0,500,501,5,99,0,0,501,502,5,97,
0,0,502,503,5,110,0,0,503,504,5,95,0,0,504,505,5,97,0,0,505,506,
5,108,0,0,506,507,5,108,0,0,507,508,5,95,0,0,508,509,5,108,0,0,509,
510,5,105,0,0,510,511,5,110,0,0,511,512,5,117,0,0,512,513,5,120,
0,0,513,514,5,95,0,0,514,515,5,107,0,0,515,516,5,101,0,0,516,517,
5,114,0,0,517,518,5,110,0,0,518,519,5,101,0,0,519,520,5,108,0,0,
520,789,5,115,0,0,521,522,5,115,0,0,522,523,5,99,0,0,523,524,5,97,
0,0,524,525,5,110,0,0,525,526,5,95,0,0,526,527,5,100,0,0,527,528,
5,101,0,0,528,529,5,108,0,0,529,530,5,97,0,0,530,789,5,121,0,0,531,
532,5,115,0,0,532,533,5,99,0,0,533,534,5,97,0,0,534,535,5,110,0,
0,535,536,5,95,0,0,536,537,5,100,0,0,537,538,5,114,0,0,538,539,5,
105,0,0,539,540,5,118,0,0,540,541,5,101,0,0,541,542,5,114,0,0,542,
543,5,95,0,0,543,544,5,100,0,0,544,545,5,105,0,0,545,546,5,114,0,
0,546,789,5,115,0,0,547,548,5,115,0,0,548,549,5,99,0,0,549,550,5,
97,0,0,550,551,5,110,0,0,551,552,5,102,0,0,552,553,5,111,0,0,553,
789,5,114,0,0,554,555,5,115,0,0,555,556,5,99,0,0,556,557,5,114,0,
0,557,558,5,101,0,0,558,559,5,101,0,0,559,560,5,110,0,0,560,561,
5,115,0,0,561,562,5,97,0,0,562,563,5,118,0,0,563,564,5,101,0,0,564,
789,5,114,0,0,565,566,5,115,0,0,566,567,5,101,0,0,567,568,5,108,
0,0,568,569,5,101,0,0,569,570,5,99,0,0,570,571,5,116,0,0,571,572,
5,105,0,0,572,573,5,111,0,0,573,574,5,110,0,0,574,575,5,95,0,0,575,
576,5,98,0,0,576,577,5,105,0,0,577,789,5,103,0,0,578,579,5,115,0,
0,579,580,5,101,0,0,580,581,5,108,0,0,581,582,5,101,0,0,582,583,
5,99,0,0,583,584,5,116,0,0,584,585,5,105,0,0,585,586,5,111,0,0,586,
587,5,110,0,0,587,588,5,95,0,0,588,589,5,115,0,0,589,590,5,109,0,
0,590,591,5,97,0,0,591,592,5,108,0,0,592,789,5,108,0,0,593,594,5,
115,0,0,594,595,5,104,0,0,595,596,5,111,0,0,596,597,5,119,0,0,597,
598,5,116,0,0,598,599,5,111,0,0,599,600,5,111,0,0,600,601,5,108,
0,0,601,789,5,115,0,0,602,603,5,115,0,0,603,604,5,104,0,0,604,605,
5,117,0,0,605,606,5,116,0,0,606,607,5,100,0,0,607,608,5,111,0,0,
608,609,5,119,0,0,609,610,5,110,0,0,610,611,5,95,0,0,611,612,5,97,
0,0,612,613,5,102,0,0,613,614,5,116,0,0,614,615,5,101,0,0,615,616,
5,114,0,0,616,617,5,95,0,0,617,618,5,116,0,0,618,619,5,105,0,0,619,
620,5,109,0,0,620,621,5,101,0,0,621,622,5,111,0,0,622,623,5,117,
0,0,623,789,5,116,0,0,624,625,5,115,0,0,625,626,5,109,0,0,626,627,
5,97,0,0,627,628,5,108,0,0,628,629,5,108,0,0,629,630,5,95,0,0,630,
631,5,105,0,0,631,632,5,99,0,0,632,633,5,111,0,0,633,634,5,110,0,
0,634,635,5,95,0,0,635,636,5,115,0,0,636,637,5,105,0,0,637,638,5,
122,0,0,638,789,5,101,0,0,639,640,5,115,0,0,640,641,5,112,0,0,641,
642,5,111,0,0,642,643,5,111,0,0,643,644,5,102,0,0,644,645,5,95,0,
0,645,646,5,111,0,0,646,647,5,115,0,0,647,648,5,120,0,0,648,649,
5,95,0,0,649,650,5,118,0,0,650,651,5,101,0,0,651,652,5,114,0,0,652,
653,5,115,0,0,653,654,5,105,0,0,654,655,5,111,0,0,655,789,5,110,
0,0,656,657,5,115,0,0,657,658,5,117,0,0,658,659,5,112,0,0,659,660,
5,112,0,0,660,661,5,111,0,0,661,662,5,114,0,0,662,663,5,116,0,0,
663,664,5,95,0,0,664,665,5,103,0,0,665,666,5,122,0,0,666,667,5,105,
0,0,667,668,5,112,0,0,668,669,5,112,0,0,669,670,5,101,0,0,670,671,
5,100,0,0,671,672,5,95,0,0,672,673,5,108,0,0,673,674,5,111,0,0,674,
675,5,97,0,0,675,676,5,100,0,0,676,677,5,101,0,0,677,678,5,114,0,
0,678,789,5,115,0,0,679,680,5,116,0,0,680,681,5,101,0,0,681,682,
5,120,0,0,682,683,5,116,0,0,683,684,5,109,0,0,684,685,5,111,0,0,
685,686,5,100,0,0,686,789,5,101,0,0,687,688,5,116,0,0,688,689,5,
101,0,0,689,690,5,120,0,0,690,691,5,116,0,0,691,692,5,111,0,0,692,
693,5,110,0,0,693,694,5,108,0,0,694,789,5,121,0,0,695,696,5,116,
0,0,696,697,5,105,0,0,697,698,5,109,0,0,698,699,5,101,0,0,699,700,
5,111,0,0,700,701,5,117,0,0,701,789,5,116,0,0,702,703,5,117,0,0,
703,704,5,101,0,0,704,705,5,102,0,0,705,706,5,105,0,0,706,707,5,
95,0,0,707,708,5,100,0,0,708,709,5,101,0,0,709,710,5,101,0,0,710,
711,5,112,0,0,711,712,5,95,0,0,712,713,5,108,0,0,713,714,5,101,0,
0,714,715,5,103,0,0,715,716,5,97,0,0,716,717,5,99,0,0,717,718,5,
121,0,0,718,719,5,95,0,0,719,720,5,115,0,0,720,721,5,99,0,0,721,
722,5,97,0,0,722,789,5,110,0,0,723,724,5,117,0,0,724,725,5,115,0,
0,725,726,5,101,0,0,726,727,5,95,0,0,727,728,5,103,0,0,728,729,5,
114,0,0,729,730,5,97,0,0,730,731,5,112,0,0,731,732,5,104,0,0,732,
733,5,105,0,0,733,734,5,99,0,0,734,735,5,115,0,0,735,736,5,95,0,
0,736,737,5,102,0,0,737,738,5,111,0,0,738,789,5,114,0,0,739,740,
5,117,0,0,740,741,5,115,0,0,741,742,5,101,0,0,742,743,5,95,0,0,743,
744,5,110,0,0,744,745,5,118,0,0,745,746,5,114,0,0,746,747,5,97,0,
0,747,789,5,109,0,0,748,749,5,119,0,0,749,750,5,105,0,0,750,751,
5,110,0,0,751,752,5,100,0,0,752,753,5,111,0,0,753,754,5,119,0,0,
754,755,5,115,0,0,755,756,5,95,0,0,756,757,5,114,0,0,757,758,5,101,
0,0,758,759,5,99,0,0,759,760,5,111,0,0,760,761,5,118,0,0,761,762,
5,101,0,0,762,763,5,114,0,0,763,764,5,121,0,0,764,765,5,95,0,0,765,
766,5,102,0,0,766,767,5,105,0,0,767,768,5,108,0,0,768,769,5,101,
0,0,769,789,5,115,0,0,770,771,5,119,0,0,771,772,5,114,0,0,772,773,
5,105,0,0,773,774,5,116,0,0,774,775,5,101,0,0,775,776,5,95,0,0,776,
777,5,115,0,0,777,778,5,121,0,0,778,779,5,115,0,0,779,780,5,116,
0,0,780,781,5,101,0,0,781,782,5,109,0,0,782,783,5,100,0,0,783,784,
5,95,0,0,784,785,5,118,0,0,785,786,5,97,0,0,786,787,5,114,0,0,787,
789,5,115,0,0,788,93,1,0,0,0,788,107,1,0,0,0,788,113,1,0,0,0,788,
125,1,0,0,0,788,138,1,0,0,0,788,148,1,0,0,0,788,165,1,0,0,0,788,
180,1,0,0,0,788,196,1,0,0,0,788,215,1,0,0,0,788,231,1,0,0,0,788,
249,1,0,0,0,788,263,1,0,0,0,788,278,1,0,0,0,788,296,1,0,0,0,788,
311,1,0,0,0,788,328,1,0,0,0,788,347,1,0,0,0,788,359,1,0,0,0,788,
371,1,0,0,0,788,399,1,0,0,0,788,417,1,0,0,0,788,432,1,0,0,0,788,
436,1,0,0,0,788,442,1,0,0,0,788,451,1,0,0,0,788,460,1,0,0,0,788,
468,1,0,0,0,788,478,1,0,0,0,788,489,1,0,0,0,788,499,1,0,0,0,788,
521,1,0,0,0,788,531,1,0,0,0,788,547,1,0,0,0,788,554,1,0,0,0,788,
565,1,0,0,0,788,578,1,0,0,0,788,593,1,0,0,0,788,602,1,0,0,0,788,
624,1,0,0,0,788,639,1,0,0,0,788,656,1,0,0,0,788,679,1,0,0,0,788,
687,1,0,0,0,788,695,1,0,0,0,788,702,1,0,0,0,788,723,1,0,0,0,788,
739,1,0,0,0,788,748,1,0,0,0,788,770,1,0,0,0,789,793,1,0,0,0,790,
792,8,1,0,0,791,790,1,0,0,0,792,795,1,0,0,0,793,791,1,0,0,0,793,
794,1,0,0,0,794,797,1,0,0,0,795,793,1,0,0,0,796,798,3,4,1,0,797,
796,1,0,0,0,797,798,1,0,0,0,798,799,1,0,0,0,799,800,6,4,0,0,800,
11,1,0,0,0,801,804,3,14,6,0,802,804,3,16,7,0,803,801,1,0,0,0,803,
802,1,0,0,0,804,13,1,0,0,0,805,806,5,109,0,0,806,807,5,101,0,0,807,
808,5,110,0,0,808,809,5,117,0,0,809,810,5,101,0,0,810,811,5,110,
0,0,811,812,5,116,0,0,812,813,5,114,0,0,813,814,5,121,0,0,814,15,
1,0,0,0,815,816,5,115,0,0,816,817,5,117,0,0,817,818,5,98,0,0,818,
819,5,109,0,0,819,820,5,101,0,0,820,821,5,110,0,0,821,822,5,117,
0,0,822,823,5,101,0,0,823,824,5,110,0,0,824,825,5,116,0,0,825,826,
5,114,0,0,826,827,5,121,0,0,827,17,1,0,0,0,828,829,5,118,0,0,829,
830,5,111,0,0,830,831,5,108,0,0,831,832,5,117,0,0,832,833,5,109,
0,0,833,834,5,101,0,0,834,19,1,0,0,0,835,836,5,108,0,0,836,837,5,
111,0,0,837,838,5,97,0,0,838,839,5,100,0,0,839,840,5,101,0,0,840,
841,5,114,0,0,841,21,1,0,0,0,842,843,5,105,0,0,843,844,5,110,0,0,
844,845,5,105,0,0,845,846,5,116,0,0,846,847,5,114,0,0,847,848,5,
100,0,0,848,23,1,0,0,0,849,850,5,105,0,0,850,851,5,99,0,0,851,852,
5,111,0,0,852,853,5,110,0,0,853,25,1,0,0,0,854,855,5,111,0,0,855,
856,5,115,0,0,856,857,5,116,0,0,857,858,5,121,0,0,858,859,5,112,
0,0,859,860,5,101,0,0,860,861,1,0,0,0,861,862,3,2,0,0,862,863,1,
0,0,0,863,864,6,12,1,0,864,27,1,0,0,0,865,866,5,103,0,0,866,867,
5,114,0,0,867,868,5,97,0,0,868,869,5,112,0,0,869,870,5,104,0,0,870,
871,5,105,0,0,871,872,5,99,0,0,872,873,5,115,0,0,873,874,1,0,0,0,
874,875,3,2,0,0,875,876,1,0,0,0,876,877,6,13,1,0,877,29,1,0,0,0,
878,879,5,111,0,0,879,880,5,112,0,0,880,881,5,116,0,0,881,882,5,
105,0,0,882,883,5,111,0,0,883,884,5,110,0,0,884,885,5,115,0,0,885,
31,1,0,0,0,886,887,5,97,0,0,887,888,5,100,0,0,888,889,5,100,0,0,
889,890,5,95,0,0,890,891,5,111,0,0,891,892,5,112,0,0,892,893,5,116,
0,0,893,894,5,105,0,0,894,895,5,111,0,0,895,896,5,110,0,0,896,897,
5,115,0,0,897,33,1,0,0,0,898,899,5,102,0,0,899,900,5,105,0,0,900,
901,5,114,0,0,901,902,5,109,0,0,902,903,5,119,0,0,903,904,5,97,0,
0,904,905,5,114,0,0,905,906,5,101,0,0,906,907,5,95,0,0,907,908,5,
98,0,0,908,909,5,111,0,0,909,910,5,111,0,0,910,911,5,116,0,0,911,
912,5,110,0,0,912,913,5,117,0,0,913,914,5,109,0,0,914,35,1,0,0,0,
915,916,5,100,0,0,916,917,5,105,0,0,917,918,5,115,0,0,918,919,5,
97,0,0,919,920,5,98,0,0,920,921,5,108,0,0,921,922,5,101,0,0,922,
923,5,100,0,0,923,37,1,0,0,0,924,925,5,105,0,0,925,926,5,110,0,0,
926,927,5,99,0,0,927,928,5,108,0,0,928,929,5,117,0,0,929,930,5,100,
0,0,930,931,5,101,0,0,931,39,1,0,0,0,932,933,5,123,0,0,933,41,1,
0,0,0,934,935,5,125,0,0,935,43,1,0,0,0,936,938,3,46,22,0,937,936,
1,0,0,0,938,939,1,0,0,0,939,937,1,0,0,0,939,940,1,0,0,0,940,45,1,
0,0,0,941,942,7,2,0,0,942,47,1,0,0,0,943,947,3,50,24,0,944,947,3,
52,25,0,945,947,3,54,26,0,946,943,1,0,0,0,946,944,1,0,0,0,946,945,
1,0,0,0,947,49,1,0,0,0,948,950,5,39,0,0,949,951,8,1,0,0,950,949,
1,0,0,0,951,952,1,0,0,0,952,950,1,0,0,0,952,953,1,0,0,0,953,954,
1,0,0,0,954,955,5,39,0,0,955,51,1,0,0,0,956,958,5,34,0,0,957,959,
8,1,0,0,958,957,1,0,0,0,959,960,1,0,0,0,960,958,1,0,0,0,960,961,
1,0,0,0,961,962,1,0,0,0,962,963,5,34,0,0,963,53,1,0,0,0,964,966,
8,3,0,0,965,964,1,0,0,0,966,967,1,0,0,0,967,965,1,0,0,0,967,968,
1,0,0,0,968,55,1,0,0,0,969,970,5,77,0,0,970,971,5,97,0,0,971,972,
5,99,0,0,972,973,5,79,0,0,973,995,5,83,0,0,974,975,5,76,0,0,975,
976,5,105,0,0,976,977,5,110,0,0,977,978,5,117,0,0,978,995,5,120,
0,0,979,980,5,69,0,0,980,981,5,76,0,0,981,982,5,73,0,0,982,983,5,
76,0,0,983,995,5,79,0,0,984,985,5,87,0,0,985,986,5,105,0,0,986,987,
5,110,0,0,987,988,5,100,0,0,988,989,5,111,0,0,989,990,5,119,0,0,
990,995,5,115,0,0,991,992,5,88,0,0,992,993,5,79,0,0,993,995,5,77,
0,0,994,969,1,0,0,0,994,974,1,0,0,0,994,979,1,0,0,0,994,984,1,0,
0,0,994,991,1,0,0,0,995,996,1,0,0,0,996,997,6,27,2,0,997,57,1,0,
0,0,998,999,5,111,0,0,999,1004,5,110,0,0,1000,1001,5,111,0,0,1001,
1002,5,102,0,0,1002,1004,5,102,0,0,1003,998,1,0,0,0,1003,1000,1,
0,0,0,1004,1005,1,0,0,0,1005,1006,6,28,2,0,1006,59,1,0,0,0,18,0,
1,63,68,75,85,89,788,793,797,803,939,946,952,960,967,994,1003,3,
6,0,0,5,1,0,4,0,0
]
class RefindConfigLexer(Lexer):
atn = ATNDeserializer().deserialize(serializedATN())
decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ]
STRICT_PARAMETER_MODE = 1
WHITESPACE = 1
NEWLINE = 2
EMPTY = 3
COMMENT = 4
IGNORED_OPTION = 5
MENU_ENTRY = 6
VOLUME = 7
LOADER = 8
INITRD = 9
ICON = 10
OS_TYPE = 11
GRAPHICS = 12
BOOT_OPTIONS = 13
ADD_BOOT_OPTIONS = 14
FIRMWARE_BOOTNUM = 15
DISABLED = 16
INCLUDE = 17
OPEN_BRACE = 18
CLOSE_BRACE = 19
HEX_INTEGER = 20
STRING = 21
OS_TYPE_PARAMETER = 22
GRAPHICS_PARAMETER = 23
channelNames = [ u"DEFAULT_TOKEN_CHANNEL", u"HIDDEN" ]
modeNames = [ "DEFAULT_MODE", "STRICT_PARAMETER_MODE" ]
literalNames = [ "<INVALID>",
"'volume'", "'loader'", "'initrd'", "'icon'", "'options'", "'add_options'",
"'firmware_bootnum'", "'disabled'", "'include'", "'{'", "'}'" ]
symbolicNames = [ "<INVALID>",
"WHITESPACE", "NEWLINE", "EMPTY", "COMMENT", "IGNORED_OPTION",
"MENU_ENTRY", "VOLUME", "LOADER", "INITRD", "ICON", "OS_TYPE",
"GRAPHICS", "BOOT_OPTIONS", "ADD_BOOT_OPTIONS", "FIRMWARE_BOOTNUM",
"DISABLED", "INCLUDE", "OPEN_BRACE", "CLOSE_BRACE", "HEX_INTEGER",
"STRING", "OS_TYPE_PARAMETER", "GRAPHICS_PARAMETER" ]
ruleNames = [ "WHITESPACE", "NEWLINE", "EMPTY", "COMMENT", "IGNORED_OPTION",
"MENU_ENTRY", "MAIN_MENU_ENTRY", "SUB_MENU_ENTRY", "VOLUME",
"LOADER", "INITRD", "ICON", "OS_TYPE", "GRAPHICS", "BOOT_OPTIONS",
"ADD_BOOT_OPTIONS", "FIRMWARE_BOOTNUM", "DISABLED", "INCLUDE",
"OPEN_BRACE", "CLOSE_BRACE", "HEX_INTEGER", "HEX_DIGIT",
"STRING", "SINGLE_QUOTED_STRING", "DOUBLE_QUOTED_STRING",
"UNQUOTED_STRING", "OS_TYPE_PARAMETER", "GRAPHICS_PARAMETER" ]
grammarFileName = "RefindConfigLexer.g4"
def __init__(self, input=None, output:TextIO = sys.stdout):
super().__init__(input, output)
self.checkVersion("4.12.0")
self._interp = LexerATNSimulator(self, self.atn, self.decisionsToDFA, PredictionContextCache())
self._actions = None
self._predicates = None

View File

@ -0,0 +1,113 @@
# Generated from c:\Users\Luka\Projects\Python\refind-btrfs\src\refind_btrfs\boot\antlr4\RefindConfigParser.g4 by ANTLR 4.12.0
from antlr4 import *
if __name__ is not None and "." in __name__:
from .RefindConfigParser import RefindConfigParser
else:
from RefindConfigParser import RefindConfigParser
# This class defines a complete generic visitor for a parse tree produced by RefindConfigParser.
class RefindConfigParserVisitor(ParseTreeVisitor):
# Visit a parse tree produced by RefindConfigParser#refind.
def visitRefind(self, ctx:RefindConfigParser.RefindContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#config_option.
def visitConfig_option(self, ctx:RefindConfigParser.Config_optionContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#boot_stanza.
def visitBoot_stanza(self, ctx:RefindConfigParser.Boot_stanzaContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#menu_entry.
def visitMenu_entry(self, ctx:RefindConfigParser.Menu_entryContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#main_option.
def visitMain_option(self, ctx:RefindConfigParser.Main_optionContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#volume.
def visitVolume(self, ctx:RefindConfigParser.VolumeContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#loader.
def visitLoader(self, ctx:RefindConfigParser.LoaderContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#main_initrd.
def visitMain_initrd(self, ctx:RefindConfigParser.Main_initrdContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#icon.
def visitIcon(self, ctx:RefindConfigParser.IconContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#os_type.
def visitOs_type(self, ctx:RefindConfigParser.Os_typeContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#graphics.
def visitGraphics(self, ctx:RefindConfigParser.GraphicsContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#main_boot_options.
def visitMain_boot_options(self, ctx:RefindConfigParser.Main_boot_optionsContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#firmware_bootnum.
def visitFirmware_bootnum(self, ctx:RefindConfigParser.Firmware_bootnumContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#disabled.
def visitDisabled(self, ctx:RefindConfigParser.DisabledContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#sub_menu.
def visitSub_menu(self, ctx:RefindConfigParser.Sub_menuContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#sub_option.
def visitSub_option(self, ctx:RefindConfigParser.Sub_optionContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#sub_initrd.
def visitSub_initrd(self, ctx:RefindConfigParser.Sub_initrdContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#sub_boot_options.
def visitSub_boot_options(self, ctx:RefindConfigParser.Sub_boot_optionsContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#add_boot_options.
def visitAdd_boot_options(self, ctx:RefindConfigParser.Add_boot_optionsContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by RefindConfigParser#include.
def visitInclude(self, ctx:RefindConfigParser.IncludeContext):
return self.visitChildren(ctx)
del RefindConfigParser

View File

@ -0,0 +1,26 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from .RefindConfigLexer import RefindConfigLexer
from .RefindConfigParser import RefindConfigParser
from .RefindConfigParserVisitor import RefindConfigParserVisitor

View File

@ -0,0 +1,230 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from __future__ import annotations
from typing import Iterable, Optional, Self
from more_itertools import last
from refind_btrfs.common import constants
from refind_btrfs.common.exceptions import RefindConfigError
from refind_btrfs.device import BlockDevice, MountOptions, Subvolume
from refind_btrfs.utility.helpers import (
has_items,
is_none_or_whitespace,
none_throws,
replace_root_part_in,
strip_quotes,
)
class BootOptions:
def __init__(self, raw_options: Optional[str]) -> None:
root_location: Optional[tuple[int, str]] = None
root_mount_options: Optional[tuple[int, MountOptions]] = None
initrd_options: list[tuple[int, str]] = []
other_options: list[tuple[int, str]] = []
if not is_none_or_whitespace(raw_options):
split_options = strip_quotes(raw_options).split()
for position, option in enumerate(split_options):
if not is_none_or_whitespace(option):
if option.startswith(constants.ROOT_PREFIX):
normalized_option = option.removeprefix(constants.ROOT_PREFIX)
if root_location is not None:
root_option = constants.ROOT_PREFIX.rstrip(
constants.PARAMETERIZED_OPTION_SEPARATOR
)
raise RefindConfigError(
f"The '{root_option}' boot option "
f"cannot be defined multiple times!"
)
root_location = (position, normalized_option)
elif option.startswith(constants.ROOTFLAGS_PREFIX):
normalized_option = option.removeprefix(
constants.ROOTFLAGS_PREFIX
)
if root_mount_options is not None:
rootflags_option = constants.ROOTFLAGS_PREFIX.rstrip(
constants.PARAMETERIZED_OPTION_SEPARATOR
)
raise RefindConfigError(
f"The '{rootflags_option}' boot option "
f"cannot be defined multiple times!"
)
root_mount_options = (position, MountOptions(normalized_option))
elif option.startswith(constants.INITRD_PREFIX):
normalized_option = option.removeprefix(constants.INITRD_PREFIX)
initrd_options.append((position, normalized_option))
else:
other_options.append((position, option))
self._root_location = root_location
self._root_mount_options = root_mount_options
self._initrd_options = initrd_options
self._other_options = other_options
def __str__(self) -> str:
root_location = self._root_location
root_mount_options = self._root_mount_options
initrd_options = self._initrd_options
other_options = self._other_options
result: list[str] = [constants.EMPTY_STR] * (
sum((len(initrd_options), len(other_options)))
+ (1 if root_location is not None else 0)
+ (1 if root_mount_options is not None else 0)
)
if root_location is not None:
result[root_location[0]] = constants.ROOT_PREFIX + root_location[1]
if root_mount_options is not None:
result[root_mount_options[0]] = constants.ROOTFLAGS_PREFIX + str(
root_mount_options[1]
)
if has_items(initrd_options):
for initrd_option in initrd_options:
result[initrd_option[0]] = constants.INITRD_PREFIX + initrd_option[1]
if has_items(other_options):
for other_option in other_options:
result[other_option[0]] = other_option[1]
if has_items(result):
joined_options = constants.BOOT_OPTION_SEPARATOR.join(result)
return constants.DOUBLE_QUOTE + joined_options + constants.DOUBLE_QUOTE
return constants.EMPTY_STR
def is_matched_with(self, block_device: BlockDevice) -> bool:
if block_device.has_root():
root_location = self.root_location
if root_location is not None:
root_partition = none_throws(block_device.root)
filesystem = none_throws(root_partition.filesystem)
normalized_root_location = last(
strip_quotes(root_location).split(
constants.PARAMETERIZED_OPTION_SEPARATOR
)
)
root_location_comparers = [
root_partition.label,
root_partition.uuid,
filesystem.label,
filesystem.uuid,
]
if (
normalized_root_location in root_location_comparers
or block_device.is_matched_with(normalized_root_location)
):
root_mount_options = self.root_mount_options
subvolume = none_throws(filesystem.subvolume)
return (
root_mount_options.is_matched_with(subvolume)
if root_mount_options is not None
else False
)
return False
def migrate_from_to(
self,
source_subvolume: Subvolume,
destination_subvolume: Subvolume,
include_paths: bool,
) -> None:
root_mount_options = self.root_mount_options
if root_mount_options is not None:
root_mount_options.migrate_from_to(source_subvolume, destination_subvolume)
if include_paths:
initrd_options = self._initrd_options
if has_items(initrd_options):
source_logical_path = source_subvolume.logical_path
destination_logical_path = destination_subvolume.logical_path
self._initrd_options = [
(
initrd_option[0],
replace_root_part_in(
initrd_option[1],
source_logical_path,
destination_logical_path,
(
constants.FORWARD_SLASH,
constants.BACKSLASH,
),
),
)
for initrd_option in initrd_options
]
@classmethod
def merge(cls, all_boot_options: Iterable[BootOptions]) -> Self:
all_boot_options_str = [
strip_quotes(str(boot_options)) for boot_options in all_boot_options
]
return cls(constants.SPACE.join(all_boot_options_str).strip())
@property
def root_location(self) -> Optional[str]:
root_location = self._root_location
if root_location is not None:
return root_location[1]
return None
@property
def root_mount_options(self) -> Optional[MountOptions]:
root_mount_options = self._root_mount_options
if root_mount_options is not None:
return root_mount_options[1]
return None
@property
def initrd_options(self) -> list[str]:
return [initrd_option[1] for initrd_option in self._initrd_options]
@property
def other_options(self) -> list[str]:
return [other_option[1] for other_option in self._other_options]

View File

@ -0,0 +1,422 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from __future__ import annotations
import inspect
import re
from collections import defaultdict
from functools import cached_property, singledispatchmethod
from itertools import chain
from typing import Any, DefaultDict, Iterable, Iterator, Optional, Self, Set
from more_itertools import always_iterable, last
from refind_btrfs.common import BootFilesCheckResult, constants
from refind_btrfs.common.enums import (
BootFilePathSource,
BootStanzaIconGenerationMode,
GraphicsParameter,
RefindOption,
)
from refind_btrfs.common.exceptions import RefindConfigError
from refind_btrfs.device import BlockDevice, Subvolume
from refind_btrfs.utility.helpers import (
has_items,
is_none_or_whitespace,
none_throws,
normalize_dir_separators_in,
strip_quotes,
)
from .boot_options import BootOptions
from .sub_menu import SubMenu
class BootStanza:
def __init__(
self,
name: str,
volume: Optional[str],
loader_path: Optional[str],
initrd_path: Optional[str],
icon_path: Optional[str],
os_type: Optional[str],
graphics: Optional[bool],
boot_options: BootOptions,
firmware_bootnum: Optional[int],
is_disabled: bool,
) -> None:
self._name = name
self._volume = volume
self._loader_path = loader_path
self._initrd_path = initrd_path
self._icon_path = icon_path
self._os_type = os_type
self._graphics = graphics
self._boot_options = boot_options
self._firmware_bootnum = firmware_bootnum
self._is_disabled = is_disabled
self._boot_files_check_result: Optional[BootFilesCheckResult] = None
self._sub_menus: Optional[list[SubMenu]] = None
def __eq__(self, other: object) -> bool:
if self is other:
return True
if isinstance(other, BootStanza):
self_boot_options = self.boot_options
other_boot_options = other.boot_options
return (
self.volume == other.volume
and self.loader_path == other.loader_path
and str(self_boot_options) == str(other_boot_options)
)
return False
def __hash__(self):
boot_options = self.boot_options
return hash((self.volume, self.loader_path, str(boot_options)))
def __str__(self) -> str:
result: list[str] = []
main_indent = constants.EMPTY_STR
option_indent = constants.TAB
name = self.name
result.append(f"{main_indent}{RefindOption.MENU_ENTRY.value} {name} {{")
icon_path = self.icon_path
if not is_none_or_whitespace(icon_path):
result.append(f"{option_indent}{RefindOption.ICON.value} {icon_path}")
volume = self.volume
if not is_none_or_whitespace(volume):
result.append(f"{option_indent}{RefindOption.VOLUME.value} {volume}")
loader_path = self.loader_path
if not is_none_or_whitespace(loader_path):
result.append(f"{option_indent}{RefindOption.LOADER.value} {loader_path}")
initrd_path = self.initrd_path
if not is_none_or_whitespace(initrd_path):
result.append(f"{option_indent}{RefindOption.INITRD.value} {initrd_path}")
os_type = self.os_type
if not is_none_or_whitespace(os_type):
result.append(f"{option_indent}{RefindOption.OS_TYPE.value} {os_type}")
graphics = self.graphics
if graphics is not None:
graphics_parameter = (
GraphicsParameter.ON if graphics else GraphicsParameter.OFF
)
result.append(
f"{option_indent}{RefindOption.GRAPHICS.value} {graphics_parameter.value}"
)
boot_options_str = str(self.boot_options)
if not is_none_or_whitespace(boot_options_str):
result.append(
f"{option_indent}{RefindOption.BOOT_OPTIONS.value} {boot_options_str}"
)
firmware_bootnum = self.firmware_bootnum
if firmware_bootnum is not None:
result.append(
f"{option_indent}{RefindOption.FIRMWARE_BOOTNUM.value} {firmware_bootnum:04x}"
)
sub_menus = self.sub_menus
if has_items(sub_menus):
result.extend(str(sub_menu) for sub_menu in none_throws(sub_menus))
is_disabled = self.is_disabled
if is_disabled:
result.append(f"{option_indent}{RefindOption.DISABLED.value}")
result.append(f"{main_indent}}}")
return constants.NEWLINE.join(result)
def with_boot_files_check_result(
self, subvolume: Subvolume, include_sub_menus: bool
) -> Self:
normalized_name = self.normalized_name
all_boot_file_paths = self.all_boot_file_paths
logical_path = subvolume.logical_path
matched_boot_files: list[str] = []
unmatched_boot_files: list[str] = []
sources = [BootFilePathSource.BOOT_STANZA]
if include_sub_menus:
sources.append(BootFilePathSource.SUB_MENU)
for source in sources:
boot_file_paths = always_iterable(all_boot_file_paths.get(source))
for boot_file_path in boot_file_paths:
append_func = (
matched_boot_files.append
if logical_path in boot_file_path
else unmatched_boot_files.append
)
append_func(boot_file_path)
self._boot_files_check_result = BootFilesCheckResult(
normalized_name, logical_path, matched_boot_files, unmatched_boot_files
)
return self
def with_sub_menus(self, sub_menus: Iterable[SubMenu]) -> Self:
self._sub_menus = list(sub_menus)
return self
@singledispatchmethod
def is_matched_with(self, argument: Any) -> bool:
frame = none_throws(inspect.currentframe())
raise NotImplementedError(
f"Cannot call the '{inspect.getframeinfo(frame).function}' method "
f"for parameter of type '{type(argument).__name__}'!"
)
def has_unmatched_boot_files(self) -> bool:
boot_files_check_result = self.boot_files_check_result
if boot_files_check_result is not None:
return boot_files_check_result.has_unmatched_boot_files()
return False
def has_sub_menus(self) -> bool:
return has_items(self.sub_menus)
def can_be_used_for_bootable_snapshot(self) -> bool:
volume = self.volume
loader_path = self.loader_path
initrd_path = self.initrd_path
is_disabled = self.is_disabled
return (
not is_none_or_whitespace(volume)
and not is_none_or_whitespace(loader_path)
and not is_none_or_whitespace(initrd_path)
and not is_disabled
)
def validate_boot_files_check_result(self) -> None:
if self.has_unmatched_boot_files():
boot_files_check_result = none_throws(self.boot_files_check_result)
boot_stanza_name = boot_files_check_result.required_by_boot_stanza_name
logical_path = boot_files_check_result.expected_logical_path
unmatched_boot_files = boot_files_check_result.unmatched_boot_files
raise RefindConfigError(
f"Detected boot files required by the '{boot_stanza_name}' boot "
f"stanza which are not matched with the '{logical_path}' subvolume: "
f"{constants.DEFAULT_ITEMS_SEPARATOR.join(unmatched_boot_files)}!"
)
def validate_icon_path(
self, icon_generation_mode: BootStanzaIconGenerationMode
) -> None:
if icon_generation_mode != BootStanzaIconGenerationMode.DEFAULT:
normalized_name = self.normalized_name
icon_path = self.icon_path
if is_none_or_whitespace(icon_path):
raise RefindConfigError(
f"The '{normalized_name}' boot stanza is missing the "
f"'{RefindOption.ICON.value}' option which must be defined in case "
f"'{icon_generation_mode.value}' is the selected mode of boot stanza "
"icon generation!"
)
@is_matched_with.register(BlockDevice)
def _is_matched_with_block_device(self, block_device: BlockDevice) -> bool:
if self.can_be_used_for_bootable_snapshot():
boot_options = self.boot_options
if boot_options.is_matched_with(block_device):
return True
else:
sub_menus = self.sub_menus
if has_items(sub_menus):
return any(
sub_menu.is_matched_with(block_device)
for sub_menu in none_throws(sub_menus)
)
return False
@is_matched_with.register(str)
def _is_matched_with_loader_filename(self, loader_filename: str) -> bool:
return self._loader_filename == loader_filename
def _get_all_boot_file_paths(
self,
) -> Iterator[tuple[BootFilePathSource, str]]:
source = BootFilePathSource.BOOT_STANZA
is_disabled = self.is_disabled
if not is_disabled:
loader_path = self.loader_path
initrd_path = self.initrd_path
boot_options = self.boot_options
if not is_none_or_whitespace(loader_path):
yield (source, none_throws(loader_path))
if not is_none_or_whitespace(initrd_path):
yield (source, none_throws(initrd_path))
yield from (
(source, initrd_option) for initrd_option in boot_options.initrd_options
)
sub_menus = self.sub_menus
if has_items(sub_menus):
yield from chain.from_iterable(
sub_menu.all_boot_file_paths for sub_menu in none_throws(sub_menus)
)
@property
def name(self) -> str:
return self._name
@property
def normalized_name(self) -> str:
return strip_quotes(self.name)
@property
def volume(self) -> Optional[str]:
return self._volume
@property
def normalized_volume(self) -> Optional[str]:
volume = self.volume
if not is_none_or_whitespace(volume):
whitespace_pattern = re.compile(constants.WHITESPACE_PATTERN)
stripped_volume = strip_quotes(volume)
return whitespace_pattern.sub("_", stripped_volume)
return None
@property
def loader_path(self) -> Optional[str]:
return self._loader_path
@property
def initrd_path(self) -> Optional[str]:
return self._initrd_path
@property
def icon_path(self) -> Optional[str]:
return self._icon_path
@property
def os_type(self) -> Optional[str]:
return self._os_type
@property
def graphics(self) -> Optional[bool]:
return self._graphics
@property
def boot_options(self) -> BootOptions:
return self._boot_options
@property
def firmware_bootnum(self) -> Optional[int]:
return self._firmware_bootnum
@property
def is_disabled(self) -> bool:
return self._is_disabled
@property
def boot_files_check_result(self) -> Optional[BootFilesCheckResult]:
return self._boot_files_check_result
@property
def sub_menus(self) -> Optional[list[SubMenu]]:
return self._sub_menus
@cached_property
def filename(self) -> str:
if self.can_be_used_for_bootable_snapshot():
normalized_volume = self.normalized_volume
loader_filename = self._loader_filename
extension = constants.CONFIG_FILE_EXTENSION
return f"{normalized_volume}_{loader_filename}{extension}".lower()
return constants.EMPTY_STR
@cached_property
def all_boot_file_paths(self) -> DefaultDict[BootFilePathSource, Set[str]]:
result = defaultdict(set)
all_boot_file_paths = self._get_all_boot_file_paths()
for boot_file_path_tuple in all_boot_file_paths:
key = boot_file_path_tuple[0]
value = normalize_dir_separators_in(boot_file_path_tuple[1])
result[key].add(value)
return result
@cached_property
def _loader_filename(self) -> str:
loader_path = self.loader_path
if not is_none_or_whitespace(loader_path):
dir_separator_pattern = re.compile(constants.DIR_SEPARATOR_PATTERN)
split_loader_path = dir_separator_pattern.split(
none_throws(self.loader_path)
)
return last(split_loader_path)
return constants.EMPTY_STR

View File

@ -0,0 +1,340 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
import re
from pathlib import Path
from typing import Iterable, Iterator
from antlr4 import CommonTokenStream, FileStream
from injector import inject
from more_itertools import last, one
from refind_btrfs.common import constants
from refind_btrfs.common.abc.factories import BaseLoggerFactory
from refind_btrfs.common.abc.providers import (
BasePackageConfigProvider,
BasePersistenceProvider,
BaseRefindConfigProvider,
)
from refind_btrfs.common.enums import ConfigInitializationType, RefindOption
from refind_btrfs.common.exceptions import RefindConfigError, RefindSyntaxError
from refind_btrfs.device import Partition
from refind_btrfs.utility.helpers import (
checked_cast,
has_items,
is_none_or_whitespace,
is_singleton,
item_count_suffix,
none_throws,
)
from .antlr4 import RefindConfigLexer, RefindConfigParser
from .boot_stanza import BootStanza
from .refind_config import RefindConfig
from .refind_listeners import RefindErrorListener
from .refind_visitors import BootStanzaVisitor, IncludeVisitor
class FileRefindConfigProvider(BaseRefindConfigProvider):
all_config_file_paths: dict[Partition, Path] = {}
@inject
def __init__(
self,
logger_factory: BaseLoggerFactory,
package_config_provider: BasePackageConfigProvider,
persistence_provider: BasePersistenceProvider,
) -> None:
self._logger = logger_factory.logger(__name__)
self._package_config_provider = package_config_provider
self._persistence_provider = persistence_provider
self._refind_configs: dict[Path, RefindConfig] = {}
def get_config(self, partition: Partition) -> RefindConfig:
logger = self._logger
config_file_path = FileRefindConfigProvider.all_config_file_paths.get(partition)
should_begin_search = config_file_path is None or not config_file_path.exists()
if should_begin_search:
package_config_provider = self._package_config_provider
package_config = package_config_provider.get_config()
boot_stanza_generation = package_config.boot_stanza_generation
refind_config_file = boot_stanza_generation.refind_config
logger.info(
f"Searching for the '{refind_config_file}' file on '{partition.name}'."
)
refind_config_search_result = partition.search_paths_for(refind_config_file)
if not has_items(refind_config_search_result):
raise RefindConfigError(
f"Could not find the '{refind_config_file}' file!"
)
if not is_singleton(refind_config_search_result):
raise RefindConfigError(
f"Found multiple '{refind_config_file}' files (at most one is expected)!"
)
config_file_path = one(none_throws(refind_config_search_result)).resolve()
FileRefindConfigProvider.all_config_file_paths[partition] = config_file_path
return self._read_config_from(none_throws(config_file_path))
def save_config(self, config: RefindConfig) -> None:
logger = self._logger
persistence_provider = self._persistence_provider
boot_stanzas = config.boot_stanzas
if has_items(boot_stanzas):
config_file_path = config.file_path
destination_directory = config_file_path.parent
refind_directory = destination_directory.parent
if not destination_directory.exists():
logger.info(
"Creating the "
f"'{destination_directory.relative_to(refind_directory)}' "
"destination directory."
)
destination_directory.mkdir()
try:
logger.info(
f"Writing to the '{config_file_path.relative_to(refind_directory)}' file."
)
with config_file_path.open("w") as config_file:
lines_for_writing: list[str] = []
lines_for_writing.append(
constants.NEWLINE.join(
str(boot_stanza)
for boot_stanza in none_throws(boot_stanzas)
)
)
lines_for_writing.append(constants.NEWLINE)
config_file.writelines(lines_for_writing)
except OSError as e:
logger.exception("Path.open('w') call failed!")
raise RefindConfigError(
f"Could not write to the '{config_file_path.name}' file!"
) from e
config.refresh_file_stat()
persistence_provider.save_refind_config(config)
def append_to_config(self, config: RefindConfig) -> None:
logger = self._logger
persistence_provider = self._persistence_provider
config_file_path = config.file_path
actual_config = persistence_provider.get_refind_config(config_file_path)
if actual_config is not None:
new_included_configs = config.get_included_configs_difference_from(
actual_config
)
if has_items(new_included_configs):
included_configs_for_appending = none_throws(new_included_configs)
try:
with config_file_path.open("r") as config_file:
all_lines = config_file.readlines()
last_line = last(all_lines)
except OSError as e:
logger.exception("Path.open('r') call failed!")
raise RefindConfigError(
f"Could not read from the '{config_file_path}' file!"
) from e
else:
include_option = RefindOption.INCLUDE.value
suffix = item_count_suffix(included_configs_for_appending)
try:
logger.info(
f"Appending {len(included_configs_for_appending)} '{include_option}' "
f"directive{suffix} to the '{config_file_path.name}' file."
)
with config_file_path.open("a") as config_file:
lines_for_appending: list[str] = []
should_prepend_newline = False
if not is_none_or_whitespace(last_line):
include_option_pattern = re.compile(
constants.INCLUDE_OPTION_PATTERN, re.DOTALL
)
should_prepend_newline = (
not include_option_pattern.match(last_line)
)
if should_prepend_newline:
lines_for_appending.append(constants.NEWLINE)
destination_directory = config_file_path.parent
for included_config in included_configs_for_appending:
included_config_relative_file_path = (
included_config.file_path.relative_to(
destination_directory
)
)
lines_for_appending.append(
f"{include_option} {included_config_relative_file_path}"
f"{constants.NEWLINE}"
)
config_file.writelines(lines_for_appending)
except OSError as e:
logger.exception("Path.open('a') call failed!")
raise RefindConfigError(
f"Could not append to the '{config_file_path.name}' file!"
) from e
config.refresh_file_stat()
persistence_provider.save_refind_config(config)
def _read_config_from(self, config_file_path: Path) -> RefindConfig:
persistence_provider = self._persistence_provider
persisted_refind_config = persistence_provider.get_refind_config(
config_file_path
)
current_refind_config = self._refind_configs.get(config_file_path)
if persisted_refind_config is None:
logger = self._logger
logger.info(f"Analyzing the '{config_file_path.name}' file.")
try:
input_stream = FileStream(str(config_file_path), encoding="utf-8")
lexer = RefindConfigLexer(input_stream)
token_stream = CommonTokenStream(lexer)
parser = RefindConfigParser(token_stream)
error_listener = RefindErrorListener()
parser.removeErrorListeners()
parser.addErrorListener(error_listener)
refind_context = parser.refind()
except RefindSyntaxError as e:
logger.exception(
f"Error while parsing the '{config_file_path.name}' file!"
)
raise RefindConfigError(
"Could not load rEFInd configuration from file!"
) from e
else:
config_option_contexts = checked_cast(
list[RefindConfigParser.Config_optionContext],
refind_context.config_option(),
)
boot_stanzas = FileRefindConfigProvider._map_to_boot_stanzas(
config_option_contexts
)
includes = FileRefindConfigProvider._map_to_includes(
config_option_contexts
)
included_configs = self._read_included_configs_from(
config_file_path.parent, includes
)
current_refind_config = (
RefindConfig(config_file_path)
.with_boot_stanzas(boot_stanzas)
.with_included_configs(included_configs)
.with_initialization_type(ConfigInitializationType.PARSED)
)
persistence_provider.save_refind_config(current_refind_config)
elif current_refind_config is None:
current_refind_config = persisted_refind_config.with_initialization_type(
ConfigInitializationType.PERSISTED
)
if current_refind_config.has_included_configs():
current_included_configs = none_throws(
current_refind_config.included_configs
)
actual_included_configs = [
self._read_config_from(included_config.file_path)
for included_config in current_included_configs
if included_config.file_path.exists()
]
current_refind_config = current_refind_config.with_included_configs(
actual_included_configs
)
self._refind_configs[config_file_path] = current_refind_config
return current_refind_config
def _read_included_configs_from(
self, root_directory: Path, includes: Iterable[str]
) -> Iterator[RefindConfig]:
logger = self._logger
for include in includes:
included_config_file_path = root_directory / include
if included_config_file_path.exists():
yield self._read_config_from(included_config_file_path.resolve())
else:
logger.warning(
f"The included config file '{included_config_file_path.name}' does not exist."
)
@staticmethod
def _map_to_boot_stanzas(
config_option_contexts: list[RefindConfigParser.Config_optionContext],
) -> Iterator[BootStanza]:
if has_items(config_option_contexts):
boot_stanza_visitor = BootStanzaVisitor()
for config_option_context in config_option_contexts:
boot_stanza_context = config_option_context.boot_stanza()
if boot_stanza_context is not None:
yield checked_cast(
BootStanza, boot_stanza_context.accept(boot_stanza_visitor)
)
@staticmethod
def _map_to_includes(
config_option_contexts: list[RefindConfigParser.Config_optionContext],
) -> Iterator[str]:
if has_items(config_option_contexts):
include_visitor = IncludeVisitor()
for config_option_context in config_option_contexts:
include_context = config_option_context.include()
if include_context is not None:
yield checked_cast(str, include_context.accept(include_visitor))

View File

@ -0,0 +1,24 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from .migration import Migration

View File

@ -0,0 +1,129 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from abc import ABC, abstractmethod
from pathlib import Path
from refind_btrfs.common import BtrfsLogo, Icon
from refind_btrfs.common.abc.commands import IconCommand
from refind_btrfs.common.enums import BootStanzaIconGenerationMode
class BaseIconMigrationStrategy(ABC):
def __init__(
self, icon_command: IconCommand, refind_config_path: Path, source_icon: str
) -> None:
self._icon_command = icon_command
self._refind_config_path = refind_config_path
self._source_icon_path = Path(source_icon)
@abstractmethod
def migrate(self) -> str:
pass
class DefaultMigrationStrategy(BaseIconMigrationStrategy):
def migrate(self) -> str:
return str(self._source_icon_path)
class CustomMigrationStrategy(BaseIconMigrationStrategy):
def __init__(
self,
icon_command: IconCommand,
refind_config_path: Path,
source_icon: str,
custom_icon_path: Path,
) -> None:
super().__init__(icon_command, refind_config_path, source_icon)
self._custom_icon_path = custom_icon_path
def migrate(self) -> str:
icon_command = self._icon_command
refind_config_path = self._refind_config_path
source_icon_path = self._source_icon_path
custom_icon_path = self._custom_icon_path
destination_icon_relative_path = icon_command.validate_custom_icon(
refind_config_path, source_icon_path, custom_icon_path
)
return str(destination_icon_relative_path)
class EmbedBtrfsLogoStrategy(BaseIconMigrationStrategy):
def __init__(
self,
icon_command: IconCommand,
refind_config_path: Path,
source_icon: str,
btrfs_logo: BtrfsLogo,
) -> None:
super().__init__(icon_command, refind_config_path, source_icon)
self._btrfs_logo = btrfs_logo
def migrate(self) -> str:
icon_command = self._icon_command
refind_config_path = self._refind_config_path
source_icon_path = self._source_icon_path
btrfs_logo = self._btrfs_logo
destination_icon_relative_path = icon_command.embed_btrfs_logo_into_source_icon(
refind_config_path, source_icon_path, btrfs_logo
)
return str(destination_icon_relative_path)
class IconMigrationFactory:
@staticmethod
def migration_strategy(
icon_command: IconCommand,
refind_config_path: Path,
source_icon: str,
icon: Icon,
) -> BaseIconMigrationStrategy:
mode = icon.mode
if mode == BootStanzaIconGenerationMode.DEFAULT:
return DefaultMigrationStrategy(
icon_command, refind_config_path, source_icon
)
if mode == BootStanzaIconGenerationMode.CUSTOM:
custom_icon_path = icon.path
return CustomMigrationStrategy(
icon_command, refind_config_path, source_icon, custom_icon_path
)
if mode == BootStanzaIconGenerationMode.EMBED_BTRFS_LOGO:
btrfs_logo = icon.btrfs_logo
return EmbedBtrfsLogoStrategy(
icon_command, refind_config_path, source_icon, btrfs_logo
)
raise ValueError(
"The 'icon' parameter's 'mode' property contains an unexpected value!"
)

View File

@ -0,0 +1,362 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
import re
from abc import ABC, abstractmethod
from copy import deepcopy
from functools import singledispatchmethod
from pathlib import Path
from typing import Any, Optional
from refind_btrfs.common import BootStanzaGeneration, Icon, constants
from refind_btrfs.common.abc.commands import IconCommand
from refind_btrfs.device import Subvolume
from refind_btrfs.utility.helpers import (
default_if_none,
is_none_or_whitespace,
none_throws,
replace_root_part_in,
)
from ..boot_options import BootOptions
from ..boot_stanza import BootStanza
from ..sub_menu import SubMenu
from .icon_migration_strategies import IconMigrationFactory
from .state import State
class BaseMainMigrationStrategy(ABC):
def __init__(
self,
is_latest: bool,
refind_config_path: Path,
current_state: State,
source_subvolume: Subvolume,
destination_subvolume: Subvolume,
boot_stanza_generation: BootStanzaGeneration,
) -> None:
self._is_latest = is_latest
self._refind_config_path = refind_config_path
self._current_state = current_state
self._source_subvolume = source_subvolume
self._destination_subvolume = destination_subvolume
self._boot_stanza_generation = boot_stanza_generation
@abstractmethod
def migrate(self) -> State:
pass
@property
def destination_name(self) -> str:
destination_subvolume = self._destination_subvolume
if not destination_subvolume.is_named():
raise ValueError("The 'destination_subvolume' instance must be named!")
current_name = self._current_state.name
destination_subvolume_name = none_throws(destination_subvolume.name)
subvolume_name_pattern = re.compile(rf"\({constants.SUBVOLUME_NAME_PATTERN}\)")
match = subvolume_name_pattern.search(current_name)
if match:
destination_name = subvolume_name_pattern.sub(
f"({destination_subvolume_name})", current_name
)
else:
destination_name = f"{current_name} ({destination_subvolume_name})"
return f"{constants.DOUBLE_QUOTE}{destination_name}{constants.DOUBLE_QUOTE}"
@property
def destination_loader_path(self) -> Optional[str]:
current_loader_path = self._current_state.loader_path
if not is_none_or_whitespace(current_loader_path):
return replace_root_part_in(
none_throws(current_loader_path),
self._source_subvolume.logical_path,
self._destination_subvolume.logical_path,
)
return None
@property
def destination_initrd_path(self) -> Optional[str]:
current_initrd_path = self._current_state.initrd_path
if not is_none_or_whitespace(current_initrd_path):
return replace_root_part_in(
none_throws(current_initrd_path),
self._source_subvolume.logical_path,
self._destination_subvolume.logical_path,
)
return None
@property
def destination_boot_options(self) -> Optional[BootOptions]:
current_boot_options = self._current_state.boot_options
if current_boot_options is not None:
destination_boot_options = deepcopy(current_boot_options)
include_paths = self.include_paths
destination_boot_options.migrate_from_to(
self._source_subvolume,
self._destination_subvolume,
include_paths,
)
return destination_boot_options
return None
@property
def destination_add_boot_options(self) -> Optional[BootOptions]:
current_add_boot_options = self._current_state.add_boot_options
if current_add_boot_options is not None:
destination_add_boot_options = deepcopy(current_add_boot_options)
include_paths = self.include_paths
destination_add_boot_options.migrate_from_to(
self._source_subvolume,
self._destination_subvolume,
include_paths,
)
return destination_add_boot_options
return None
@property
def include_paths(self) -> bool:
return self._boot_stanza_generation.include_paths
@property
def icon(self) -> Icon:
return self._boot_stanza_generation.icon
class BootStanzaMigrationStrategy(BaseMainMigrationStrategy):
def __init__(
self,
is_latest: bool,
refind_config_path: Path,
boot_stanza: BootStanza,
source_subvolume: Subvolume,
destination_subvolume: Subvolume,
boot_stanza_generation: BootStanzaGeneration,
icon_command: IconCommand,
) -> None:
super().__init__(
is_latest,
refind_config_path,
State(
boot_stanza.normalized_name,
boot_stanza.loader_path,
boot_stanza.initrd_path,
boot_stanza.icon_path,
boot_stanza.boot_options,
None,
),
source_subvolume,
destination_subvolume,
boot_stanza_generation,
)
self._icon_command = icon_command
def migrate(self) -> State:
include_paths = self.include_paths
is_latest = self._is_latest
current_state = self._current_state
destination_loader_path = constants.EMPTY_STR
destination_initrd_path = constants.EMPTY_STR
if is_latest:
destination_loader_path = none_throws(current_state.loader_path)
destination_initrd_path = none_throws(current_state.initrd_path)
if include_paths:
destination_loader_path = none_throws(self.destination_loader_path)
destination_initrd_path_candidate = self.destination_initrd_path
if not is_none_or_whitespace(destination_initrd_path_candidate):
destination_initrd_path = none_throws(destination_initrd_path_candidate)
icon_migration_strategy = IconMigrationFactory.migration_strategy(
self._icon_command,
self._refind_config_path,
default_if_none(current_state.icon_path, constants.EMPTY_STR),
self.icon,
)
destination_icon_path = icon_migration_strategy.migrate()
return State(
self.destination_name,
destination_loader_path,
destination_initrd_path,
destination_icon_path,
self.destination_boot_options,
None,
)
class SubMenuMigrationStrategy(BaseMainMigrationStrategy):
def __init__(
self,
is_latest: bool,
refind_config_path: Path,
sub_menu: SubMenu,
source_subvolume: Subvolume,
destination_subvolume: Subvolume,
boot_stanza_generation: BootStanzaGeneration,
inherit_from_state: State,
) -> None:
super().__init__(
is_latest,
refind_config_path,
State(
sub_menu.normalized_name,
sub_menu.loader_path,
sub_menu.initrd_path,
None,
sub_menu.boot_options,
sub_menu.add_boot_options,
),
source_subvolume,
destination_subvolume,
boot_stanza_generation,
)
self._inherit_from_state = inherit_from_state
def migrate(self) -> State:
include_paths = self.include_paths
is_latest = self._is_latest
current_state = self._current_state
inherit_from_state = self._inherit_from_state
destination_loader_path = current_state.loader_path
destination_initrd_path = current_state.initrd_path
destination_boot_options: Optional[BootOptions] = None
destination_add_boot_options = self.destination_add_boot_options
if not is_latest:
if include_paths:
destination_loader_path = inherit_from_state.loader_path
destination_initrd_path = inherit_from_state.initrd_path
destination_boot_options = BootOptions.merge(
(
none_throws(inherit_from_state.boot_options),
none_throws(destination_add_boot_options),
)
)
destination_add_boot_options = BootOptions(constants.EMPTY_STR)
if include_paths:
destination_loader_path_candidate = self.destination_loader_path
destination_initrd_path_candidate = self.destination_initrd_path
if not is_none_or_whitespace(destination_loader_path_candidate):
destination_loader_path = destination_loader_path_candidate
if not is_none_or_whitespace(destination_initrd_path_candidate):
destination_initrd_path = destination_initrd_path_candidate
return State(
self.destination_name,
destination_loader_path,
destination_initrd_path,
None,
destination_boot_options,
destination_add_boot_options,
)
class MainMigrationFactory:
# pylint: disable=unused-argument
@singledispatchmethod
@staticmethod
def migration_strategy(
argument: Any,
is_latest: bool,
refind_config_path: Path,
source_subvolume: Subvolume,
destination_subvolume: Subvolume,
boot_stanza_generation: BootStanzaGeneration,
icon_command: Optional[IconCommand] = None,
inherit_from_state: Optional[State] = None,
) -> BaseMainMigrationStrategy:
raise NotImplementedError(
"Cannot instantiate the main migration strategy "
f"for parameter of type '{type(argument).__name__}'!"
)
# pylint: disable=unused-argument
@migration_strategy.register(BootStanza)
@staticmethod
def _boot_stanza_overload(
boot_stanza: BootStanza,
is_latest: bool,
refind_config_path: Path,
source_subvolume: Subvolume,
destination_subvolume: Subvolume,
boot_stanza_generation: BootStanzaGeneration,
icon_command: Optional[IconCommand] = None,
inherit_from_state: Optional[State] = None,
) -> BaseMainMigrationStrategy:
return BootStanzaMigrationStrategy(
is_latest,
refind_config_path,
boot_stanza,
source_subvolume,
destination_subvolume,
boot_stanza_generation,
none_throws(icon_command),
)
# pylint: disable=unused-argument
@migration_strategy.register(SubMenu)
@staticmethod
def _sub_menu_overload(
sub_menu: SubMenu,
is_latest: bool,
refind_config_path: Path,
source_subvolume: Subvolume,
destination_subvolume: Subvolume,
boot_stanza_generation: BootStanzaGeneration,
icon_command: Optional[IconCommand] = None,
inherit_from_state: Optional[State] = None,
) -> BaseMainMigrationStrategy:
return SubMenuMigrationStrategy(
is_latest,
refind_config_path,
sub_menu,
source_subvolume,
destination_subvolume,
boot_stanza_generation,
none_throws(inherit_from_state),
)

View File

@ -0,0 +1,174 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from pathlib import Path
from typing import Collection, Iterator, Optional
from more_itertools import first
from refind_btrfs.common import BootStanzaGeneration, constants
from refind_btrfs.common.abc.commands import IconCommand
from refind_btrfs.common.exceptions import RefindConfigError
from refind_btrfs.device import BlockDevice, Subvolume
from refind_btrfs.utility.helpers import has_items, none_throws
from ..boot_options import BootOptions
from ..boot_stanza import BootStanza
from ..sub_menu import SubMenu
from .main_migration_strategies import MainMigrationFactory
from .state import State
class Migration:
def __init__(
self,
boot_stanza: BootStanza,
block_device: BlockDevice,
bootable_snapshots: Collection[Subvolume],
) -> None:
assert has_items(
bootable_snapshots
), "Parameter 'bootable_snapshots' must contain at least one item!"
if not boot_stanza.is_matched_with(block_device):
raise RefindConfigError("Boot stanza is not matched with the partition!")
root_partition = none_throws(block_device.root)
filesystem = none_throws(root_partition.filesystem)
source_subvolume = none_throws(filesystem.subvolume)
self._boot_stanza = boot_stanza
self._source_subvolume = source_subvolume
self._bootable_snapshots = list(bootable_snapshots)
def migrate(
self,
refind_config_path: Path,
boot_stanza_generation: BootStanzaGeneration,
icon_command: IconCommand,
) -> BootStanza:
boot_stanza = self._boot_stanza
source_subvolume = self._source_subvolume
bootable_snapshots = self._bootable_snapshots
include_sub_menus = boot_stanza_generation.include_sub_menus
latest_migration_result: Optional[State] = None
result_sub_menus: list[SubMenu] = []
for destination_subvolume in bootable_snapshots:
is_latest = self._is_latest_snapshot(destination_subvolume)
boot_stanza_migration_strategy = MainMigrationFactory.migration_strategy(
boot_stanza,
is_latest,
refind_config_path,
source_subvolume,
destination_subvolume,
boot_stanza_generation,
icon_command=icon_command,
)
migration_result = boot_stanza_migration_strategy.migrate()
if is_latest:
latest_migration_result = migration_result
else:
result_sub_menus.append(
SubMenu(
migration_result.name,
migration_result.loader_path,
migration_result.initrd_path,
boot_stanza.graphics,
migration_result.boot_options,
BootOptions(constants.EMPTY_STR),
boot_stanza.is_disabled,
)
)
if include_sub_menus:
migrated_sub_menus = self._migrate_sub_menus(
refind_config_path,
source_subvolume,
destination_subvolume,
migration_result,
boot_stanza_generation,
)
result_sub_menus.extend(list(migrated_sub_menus))
boot_stanza_migration_result = none_throws(latest_migration_result)
return BootStanza(
boot_stanza_migration_result.name,
boot_stanza.volume,
boot_stanza_migration_result.loader_path,
boot_stanza_migration_result.initrd_path,
boot_stanza_migration_result.icon_path,
boot_stanza.os_type,
boot_stanza.graphics,
none_throws(boot_stanza_migration_result.boot_options),
boot_stanza.firmware_bootnum,
boot_stanza.is_disabled,
).with_sub_menus(result_sub_menus)
def _migrate_sub_menus(
self,
refind_config_path: Path,
source_subvolume: Subvolume,
destination_subvolume: Subvolume,
boot_stanza_result: State,
boot_stanza_generation: BootStanzaGeneration,
) -> Iterator[SubMenu]:
boot_stanza = self._boot_stanza
if not boot_stanza.has_sub_menus():
return
current_sub_menus = none_throws(boot_stanza.sub_menus)
is_latest = self._is_latest_snapshot(destination_subvolume)
for sub_menu in current_sub_menus:
if sub_menu.can_be_used_for_bootable_snapshot():
sub_menu_migration_strategy = MainMigrationFactory.migration_strategy(
sub_menu,
is_latest,
refind_config_path,
source_subvolume,
destination_subvolume,
boot_stanza_generation,
inherit_from_state=boot_stanza_result,
)
migration_result = sub_menu_migration_strategy.migrate()
yield SubMenu(
migration_result.name,
migration_result.loader_path,
migration_result.initrd_path,
sub_menu.graphics,
migration_result.boot_options,
none_throws(migration_result.add_boot_options),
sub_menu.is_disabled,
)
def _is_latest_snapshot(self, snapshot: Subvolume) -> bool:
bootable_snapshots = self._bootable_snapshots
latest_snapshot = first(bootable_snapshots)
return snapshot == latest_snapshot

View File

@ -0,0 +1,35 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from typing import NamedTuple, Optional
from ..boot_options import BootOptions
class State(NamedTuple):
name: str
loader_path: Optional[str]
initrd_path: Optional[str]
icon_path: Optional[str]
boot_options: Optional[BootOptions]
add_boot_options: Optional[BootOptions]

View File

@ -0,0 +1,200 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from __future__ import annotations
from copy import copy
from itertools import chain
from pathlib import Path
from typing import Collection, Iterable, Iterator, Optional, Self
from more_itertools import always_iterable
from refind_btrfs.common import BootStanzaGeneration, constants
from refind_btrfs.common.abc import BaseConfig
from refind_btrfs.common.abc.factories import BaseIconCommandFactory
from refind_btrfs.common.enums import ConfigInitializationType
from refind_btrfs.device import BlockDevice, Subvolume
from refind_btrfs.utility.helpers import (
has_items,
is_none_or_whitespace,
none_throws,
replace_item_in,
)
from .boot_stanza import BootStanza
from .migrations import Migration
class RefindConfig(BaseConfig):
def __init__(self, file_path: Path) -> None:
super().__init__(file_path)
self._boot_stanzas: Optional[list[BootStanza]] = None
self._included_configs: Optional[list[RefindConfig]] = None
def with_boot_stanzas(self, boot_stanzas: Iterable[BootStanza]) -> Self:
self._boot_stanzas = list(boot_stanzas)
return self
def with_included_configs(self, include_configs: Iterable[RefindConfig]) -> Self:
self._included_configs = list(include_configs)
return self
def get_boot_stanzas_matched_with(
self, block_device: BlockDevice
) -> Iterator[BootStanza]:
if self.has_boot_stanzas():
yield from (
boot_stanza
for boot_stanza in none_throws(self.boot_stanzas)
if boot_stanza.is_matched_with(block_device)
)
if self.has_included_configs():
yield from chain.from_iterable(
config.get_boot_stanzas_matched_with(block_device)
for config in none_throws(self.included_configs)
)
def get_included_configs_difference_from(
self, other: RefindConfig
) -> Optional[Collection[RefindConfig]]:
if self.has_included_configs():
self_included_configs = none_throws(self.included_configs)
if not other.has_included_configs():
return self_included_configs
other_included_configs = none_throws(other.included_configs)
return set(
included_config
for included_config in self_included_configs
if included_config not in other_included_configs
)
return None
def generate_new_from(
self,
block_device: BlockDevice,
boot_stanzas_with_snapshots: dict[BootStanza, list[Subvolume]],
boot_stanza_generation: BootStanzaGeneration,
icon_command_factory: BaseIconCommandFactory,
) -> Iterator[RefindConfig]:
file_path = self.file_path
boot_stanzas = copy(none_throws(self.boot_stanzas))
parent_directory = file_path.parent
included_configs: list[RefindConfig] = (
none_throws(self.included_configs) if self.has_included_configs() else []
)
boot_stanzas.extend(
chain.from_iterable(
(
none_throws(included_config.boot_stanzas)
for included_config in included_configs
if included_config.has_boot_stanzas()
)
)
)
icon_command = icon_command_factory.icon_command()
for boot_stanza in boot_stanzas:
bootable_snapshots = boot_stanzas_with_snapshots.get(boot_stanza)
if has_items(bootable_snapshots):
sorted_bootable_snapshots = sorted(
none_throws(bootable_snapshots), reverse=True
)
migration = Migration(
boot_stanza, block_device, sorted_bootable_snapshots
)
migrated_boot_stanza = migration.migrate(
file_path, boot_stanza_generation, icon_command
)
boot_stanza_filename = migrated_boot_stanza.filename
if not is_none_or_whitespace(boot_stanza_filename):
destination_directory = (
parent_directory / constants.SNAPSHOT_STANZAS_DIR_NAME
)
boot_stanza_config_file_path = (
destination_directory / boot_stanza_filename
)
boot_stanza_config = RefindConfig(
boot_stanza_config_file_path.resolve()
).with_boot_stanzas(always_iterable(migrated_boot_stanza))
if boot_stanza_config not in included_configs:
included_configs.append(boot_stanza_config)
else:
replace_item_in(included_configs, boot_stanza_config)
yield boot_stanza_config
self._included_configs = included_configs
def has_boot_stanzas(self) -> bool:
return has_items(self.boot_stanzas)
def has_included_configs(self) -> bool:
return has_items(self.included_configs)
def is_of_initialization_type(
self, initialization_type: ConfigInitializationType
) -> bool:
if super().is_of_initialization_type(initialization_type):
return True
if self.has_included_configs():
nongenerated_included_configs = (
included_config
for included_config in none_throws(self.included_configs)
if not included_config.is_generated()
)
return any(
included_config.is_of_initialization_type(initialization_type)
for included_config in nongenerated_included_configs
)
return False
def is_generated(self) -> bool:
file_path = self.file_path
parent_directory = file_path.parent
return parent_directory.name == constants.SNAPSHOT_STANZAS_DIR_NAME
@property
def boot_stanzas(self) -> Optional[list[BootStanza]]:
return self._boot_stanzas
@property
def included_configs(self) -> Optional[list[RefindConfig]]:
return self._included_configs

View File

@ -0,0 +1,32 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from antlr4.error.ErrorListener import ErrorListener
from refind_btrfs.common.exceptions import RefindSyntaxError
class RefindErrorListener(ErrorListener):
# pylint: disable=unused-argument
def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
raise RefindSyntaxError(line, column, msg)

View File

@ -0,0 +1,340 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from collections import defaultdict
from typing import Any, Callable, DefaultDict, Iterable, NamedTuple, Optional
from antlr4 import ParserRuleContext
from more_itertools import always_iterable, only
from refind_btrfs.common import constants
from refind_btrfs.common.enums import GraphicsParameter, OSTypeParameter, RefindOption
from refind_btrfs.common.exceptions import RefindConfigError
from refind_btrfs.utility.helpers import checked_cast, try_parse_int
from .antlr4 import RefindConfigParser, RefindConfigParserVisitor
from .boot_options import BootOptions
from .boot_stanza import BootStanza
from .sub_menu import SubMenu
class ContextWithVisitor(NamedTuple):
child_context_func: Callable[[ParserRuleContext], ParserRuleContext]
visitor_func: Callable[[], RefindConfigParserVisitor]
class BootStanzaVisitor(RefindConfigParserVisitor):
def visitBoot_stanza(
self, ctx: RefindConfigParser.Boot_stanzaContext
) -> BootStanza:
menu_entry_context = ctx.menu_entry()
menu_entry = menu_entry_context.accept(MenuEntryVisitor())
main_options = OptionVisitor.map_to_options_dict(
checked_cast(list[ParserRuleContext], ctx.main_option())
)
volume = only(always_iterable(main_options.get(RefindOption.VOLUME)))
loader = only(always_iterable(main_options.get(RefindOption.LOADER)))
initrd = only(always_iterable(main_options.get(RefindOption.INITRD)))
icon = only(always_iterable(main_options.get(RefindOption.ICON)))
os_type = only(always_iterable(main_options.get(RefindOption.OS_TYPE)))
graphics = only(always_iterable(main_options.get(RefindOption.GRAPHICS)))
boot_options = only(
always_iterable(main_options.get(RefindOption.BOOT_OPTIONS))
)
firmware_bootnum = only(
always_iterable(main_options.get(RefindOption.FIRMWARE_BOOTNUM))
)
disabled = only(
always_iterable(main_options.get(RefindOption.DISABLED)), default=False
)
sub_menus = always_iterable(main_options.get(RefindOption.SUB_MENU_ENTRY))
return BootStanza(
menu_entry,
volume,
loader,
initrd,
icon,
os_type,
graphics,
BootOptions(boot_options),
firmware_bootnum,
disabled,
).with_sub_menus(sub_menus)
class MenuEntryVisitor(RefindConfigParserVisitor):
def visitMenu_entry(self, ctx: RefindConfigParser.Menu_entryContext) -> str:
token = ctx.STRING()
return token.getText()
class OptionVisitor(RefindConfigParserVisitor):
def __init__(self) -> None:
self._main_option_mappings = {
RefindOption.VOLUME: ContextWithVisitor(
RefindConfigParser.Main_optionContext.volume, VolumeVisitor
),
RefindOption.LOADER: ContextWithVisitor(
RefindConfigParser.Main_optionContext.loader, LoaderVisitor
),
RefindOption.INITRD: ContextWithVisitor(
RefindConfigParser.Main_optionContext.main_initrd, InitrdVisitor
),
RefindOption.ICON: ContextWithVisitor(
RefindConfigParser.Main_optionContext.icon, IconVisitor
),
RefindOption.OS_TYPE: ContextWithVisitor(
RefindConfigParser.Main_optionContext.os_type, OsTypeVisitor
),
RefindOption.GRAPHICS: ContextWithVisitor(
RefindConfigParser.Main_optionContext.graphics, GraphicsVisitor
),
RefindOption.BOOT_OPTIONS: ContextWithVisitor(
RefindConfigParser.Main_optionContext.main_boot_options,
BootOptionsVisitor,
),
RefindOption.FIRMWARE_BOOTNUM: ContextWithVisitor(
RefindConfigParser.Main_optionContext.firmware_bootnum,
FirmwareBootnumVisitor,
),
RefindOption.DISABLED: ContextWithVisitor(
RefindConfigParser.Main_optionContext.disabled, DisabledVisitor
),
RefindOption.SUB_MENU_ENTRY: ContextWithVisitor(
RefindConfigParser.Main_optionContext.sub_menu, SubMenuVisitor
),
}
self._sub_option_mappings = {
RefindOption.LOADER: ContextWithVisitor(
RefindConfigParser.Sub_optionContext.loader, LoaderVisitor
),
RefindOption.INITRD: ContextWithVisitor(
RefindConfigParser.Sub_optionContext.sub_initrd, InitrdVisitor
),
RefindOption.GRAPHICS: ContextWithVisitor(
RefindConfigParser.Sub_optionContext.graphics, GraphicsVisitor
),
RefindOption.BOOT_OPTIONS: ContextWithVisitor(
RefindConfigParser.Sub_optionContext.sub_boot_options,
BootOptionsVisitor,
),
RefindOption.ADD_BOOT_OPTIONS: ContextWithVisitor(
RefindConfigParser.Sub_optionContext.add_boot_options,
BootOptionsVisitor,
),
RefindOption.DISABLED: ContextWithVisitor(
RefindConfigParser.Sub_optionContext.disabled, DisabledVisitor
),
}
@classmethod
def map_to_options_dict(
cls, option_contexts: Iterable[ParserRuleContext]
) -> DefaultDict[RefindOption, list[Any]]:
option_visitor = cls()
result = defaultdict(list)
for option_context in option_contexts:
option_tuple = option_context.accept(option_visitor)
if option_tuple is not None:
key = checked_cast(RefindOption, option_tuple[0])
value = option_tuple[1]
result[key].append(value)
return result
def visitMain_option(
self, ctx: RefindConfigParser.Main_optionContext
) -> Optional[tuple[RefindOption, Any]]:
return OptionVisitor._map_to_option_tuple(ctx, self._main_option_mappings)
def visitSub_option(
self, ctx: RefindConfigParser.Sub_optionContext
) -> Optional[tuple[RefindOption, Any]]:
return OptionVisitor._map_to_option_tuple(ctx, self._sub_option_mappings)
@staticmethod
def _map_to_option_tuple(
ctx: ParserRuleContext, mappings: dict[RefindOption, ContextWithVisitor]
) -> Optional[tuple[RefindOption, Any]]:
for key, value in mappings.items():
option_context = value.child_context_func(ctx)
if option_context is not None:
visitor = value.visitor_func()
return key, option_context.accept(visitor)
return None
class SubMenuVisitor(RefindConfigParserVisitor):
def visitSub_menu(self, ctx: RefindConfigParser.Sub_menuContext) -> SubMenu:
menu_entry_context = ctx.menu_entry()
menu_entry = menu_entry_context.accept(MenuEntryVisitor())
sub_options = OptionVisitor.map_to_options_dict(
checked_cast(list[ParserRuleContext], ctx.sub_option())
)
loader = only(always_iterable(sub_options.get(RefindOption.LOADER)))
initrd = only(always_iterable(sub_options.get(RefindOption.INITRD)))
graphics = only(always_iterable(sub_options.get(RefindOption.GRAPHICS)))
boot_options = only(always_iterable(sub_options.get(RefindOption.BOOT_OPTIONS)))
add_boot_options = only(
always_iterable(sub_options.get(RefindOption.ADD_BOOT_OPTIONS))
)
disabled = only(
always_iterable(sub_options.get(RefindOption.DISABLED)), default=False
)
return SubMenu(
menu_entry,
loader,
initrd,
graphics,
BootOptions(boot_options) if boot_options is not None else None,
BootOptions(add_boot_options),
disabled,
)
class VolumeVisitor(RefindConfigParserVisitor):
def visitVolume(self, ctx: RefindConfigParser.VolumeContext) -> str:
if ctx is not None:
token = ctx.STRING()
return token.getText()
return None
class LoaderVisitor(RefindConfigParserVisitor):
def visitLoader(self, ctx: RefindConfigParser.LoaderContext) -> str:
token = ctx.STRING()
return token.getText()
class InitrdVisitor(RefindConfigParserVisitor):
def visitMain_initrd(self, ctx: RefindConfigParser.Main_initrdContext) -> str:
token = ctx.STRING()
return token.getText()
def visitSub_initrd(self, ctx: RefindConfigParser.Sub_initrdContext) -> str:
token = ctx.STRING()
if token is not None:
return token.getText()
return constants.EMPTY_STR
class IconVisitor(RefindConfigParserVisitor):
def visitIcon(self, ctx: RefindConfigParser.IconContext) -> str:
token = ctx.STRING()
return token.getText()
class OsTypeVisitor(RefindConfigParserVisitor):
def visitOs_type(self, ctx: RefindConfigParser.Os_typeContext) -> str:
token = ctx.OS_TYPE_PARAMETER()
text = token.getText()
os_type_options = [
os_type_parameter.value for os_type_parameter in OSTypeParameter
]
if text not in os_type_options:
raise RefindConfigError(f"Unexpected 'os_type' option - '{text}'!")
return text
class GraphicsVisitor(RefindConfigParserVisitor):
def visitGraphics(self, ctx: RefindConfigParser.GraphicsContext) -> bool:
token = ctx.GRAPHICS_PARAMETER()
text = token.getText()
if text == GraphicsParameter.ON.value:
return True
if text == GraphicsParameter.OFF.value:
return False
raise RefindConfigError(f"Unexpected 'graphics' option - '{text}'!")
class BootOptionsVisitor(RefindConfigParserVisitor):
def visitMain_boot_options(
self, ctx: RefindConfigParser.Main_boot_optionsContext
) -> str:
token = ctx.STRING()
return token.getText()
def visitSub_boot_options(
self, ctx: RefindConfigParser.Sub_boot_optionsContext
) -> str:
token = ctx.STRING()
if token is not None:
return token.getText()
return constants.EMPTY_STR
def visitAdd_boot_options(
self, ctx: RefindConfigParser.Add_boot_optionsContext
) -> str:
token = ctx.STRING()
return token.getText()
class FirmwareBootnumVisitor(RefindConfigParserVisitor):
def visitFirmware_bootnum(
self, ctx: RefindConfigParser.Firmware_bootnumContext
) -> int:
token = ctx.HEX_INTEGER()
text = token.getText()
firmware_bootnum = try_parse_int(text, 16)
if firmware_bootnum is None:
raise RefindConfigError(f"Unexpected 'firmware_bootnum' option - '{text}'!")
return firmware_bootnum
class DisabledVisitor(RefindConfigParserVisitor):
def visitDisabled(self, ctx: RefindConfigParser.DisabledContext) -> bool:
return True
class IncludeVisitor(RefindConfigParserVisitor):
def visitInclude(self, ctx: RefindConfigParser.IncludeContext) -> str:
token = ctx.STRING()
return token.getText()

View File

@ -0,0 +1,201 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from functools import cached_property
from typing import Iterator, Optional, Set
from refind_btrfs.common import constants
from refind_btrfs.common.enums import (
BootFilePathSource,
GraphicsParameter,
RefindOption,
)
from refind_btrfs.device import BlockDevice
from refind_btrfs.utility.helpers import (
is_empty,
is_none_or_whitespace,
none_throws,
strip_quotes,
)
from .boot_options import BootOptions
class SubMenu:
def __init__(
self,
name: str,
loader_path: Optional[str],
initrd_path: Optional[str],
graphics: Optional[bool],
boot_options: Optional[BootOptions],
add_boot_options: BootOptions,
is_disabled: bool,
) -> None:
self._name = name
self._loader_path = loader_path
self._initrd_path = initrd_path
self._graphics = graphics
self._boot_options = boot_options
self._add_boot_options = add_boot_options
self._is_disabled = is_disabled
def __str__(self) -> str:
main_indent = constants.TAB
option_indent = main_indent * 2
result: list[str] = []
name = self.name
result.append(f"{main_indent}{RefindOption.SUB_MENU_ENTRY.value} {name} {{")
loader_path = self.loader_path
if not is_none_or_whitespace(loader_path):
result.append(f"{option_indent}{RefindOption.LOADER.value} {loader_path}")
initrd_path = self.initrd_path
if not is_none_or_whitespace(initrd_path):
result.append(f"{option_indent}{RefindOption.INITRD.value} {initrd_path}")
graphics = self.graphics
if graphics is not None:
value = (
GraphicsParameter.ON.value if graphics else GraphicsParameter.OFF.value
)
result.append(f"{option_indent}{RefindOption.GRAPHICS.value} {value}")
boot_options = self.boot_options
if not boot_options is None:
boot_options_str = str(boot_options)
if not is_none_or_whitespace(boot_options_str):
result.append(
f"{option_indent}{RefindOption.BOOT_OPTIONS.value} {boot_options_str}"
)
add_boot_options_str = str(self.add_boot_options)
if not is_none_or_whitespace(add_boot_options_str):
result.append(
f"{option_indent}{RefindOption.ADD_BOOT_OPTIONS.value} {add_boot_options_str}"
)
is_disabled = self.is_disabled
if is_disabled:
result.append(f"{option_indent}{RefindOption.DISABLED.value}")
result.append(f"{main_indent}}}")
return constants.NEWLINE.join(result)
def is_matched_with(self, block_device: BlockDevice) -> bool:
boot_options = self.boot_options
return (
boot_options.is_matched_with(block_device)
if boot_options is not None
else False
)
def can_be_used_for_bootable_snapshot(self) -> bool:
loader_path = self.loader_path
initrd_path = self.initrd_path
boot_options = self.boot_options
is_disabled = self.is_disabled
return (
is_none_or_whitespace(loader_path)
and (initrd_path is None or not is_empty(initrd_path))
and boot_options is None
and not is_disabled
)
def _get_all_boot_file_paths(
self,
) -> Iterator[tuple[BootFilePathSource, str]]:
source = BootFilePathSource.SUB_MENU
is_disabled = self.is_disabled
if not is_disabled:
loader_path = self.loader_path
initrd_path = self.initrd_path
boot_options = self.boot_options
add_boot_options = self.add_boot_options
if not is_none_or_whitespace(loader_path):
yield (source, none_throws(loader_path))
if not is_none_or_whitespace(initrd_path):
yield (source, none_throws(initrd_path))
if not boot_options is None:
yield from (
(source, initrd_option)
for initrd_option in boot_options.initrd_options
)
yield from (
(source, initrd_option)
for initrd_option in add_boot_options.initrd_options
)
@property
def name(self) -> str:
return self._name
@property
def normalized_name(self) -> str:
return strip_quotes(self.name)
@property
def loader_path(self) -> Optional[str]:
return self._loader_path
@property
def initrd_path(self) -> Optional[str]:
return self._initrd_path
@property
def graphics(self) -> Optional[bool]:
return self._graphics
@property
def boot_options(self) -> Optional[BootOptions]:
return self._boot_options
@property
def add_boot_options(self) -> BootOptions:
return self._add_boot_options
@property
def is_disabled(self) -> bool:
return self._is_disabled
@cached_property
def all_boot_file_paths(self) -> Set[tuple[BootFilePathSource, str]]:
return set(self._get_all_boot_file_paths())

View File

@ -0,0 +1,34 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from .boot_files_check_result import BootFilesCheckResult
from .checkable_observer import CheckableObserver
from .configurable_mixin import ConfigurableMixin
from .package_config import (
BootStanzaGeneration,
BtrfsLogo,
Icon,
PackageConfig,
SnapshotManipulation,
SnapshotSearch,
)

View File

@ -0,0 +1,25 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from .base_config import BaseConfig
from .base_runner import BaseRunner

View File

@ -0,0 +1,110 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from __future__ import annotations
from abc import ABC
from os import stat_result
from pathlib import Path
from typing import Any, Optional, Self
from refind_btrfs.common.enums import ConfigInitializationType
from refind_btrfs.utility.helpers import checked_cast, none_throws
class BaseConfig(ABC):
def __init__(self, file_path: Path) -> None:
self._file_path = file_path
self._file_stat: Optional[stat_result] = None
self._initialization_type: Optional[ConfigInitializationType] = None
self.refresh_file_stat()
def __eq__(self, other: object) -> bool:
if self is other:
return True
if isinstance(other, BaseConfig):
self_file_path_resolved = self.file_path.resolve()
other_file_path_resolved = other.file_path.resolve()
return self_file_path_resolved == other_file_path_resolved
return False
def __hash__(self) -> int:
return hash(self.file_path.resolve())
def __getstate__(self) -> dict[str, Any]:
state = self.__dict__.copy()
initialization_type_key = "_initialization_type"
if initialization_type_key in state:
del state[initialization_type_key]
return state
def with_initialization_type(
self, initialization_type: ConfigInitializationType
) -> Self:
self._initialization_type = initialization_type
return self
def refresh_file_stat(self):
file_path = self.file_path
if file_path.exists():
self._file_stat = file_path.stat()
def is_modified(self, actual_file_path: Path) -> bool:
current_file_path = self.file_path
if current_file_path != actual_file_path:
return True
current_file_stat = none_throws(self.file_stat)
if actual_file_path.exists():
actual_file_stat = actual_file_path.stat()
return current_file_stat.st_mtime != actual_file_stat.st_mtime
return True
def is_of_initialization_type(
self, initialization_type: ConfigInitializationType
) -> bool:
return self.initialization_type == initialization_type
@property
def file_path(self) -> Path:
return self._file_path
@property
def file_stat(self) -> Optional[stat_result]:
return self._file_stat
@property
def initialization_type(self) -> Optional[ConfigInitializationType]:
return self._initialization_type

View File

@ -0,0 +1,30 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from abc import ABC, abstractmethod
class BaseRunner(ABC):
@abstractmethod
def run(self) -> int:
pass

View File

@ -0,0 +1,26 @@
# region Licensing
# SPDX-FileCopyrightText: 2020-2023 Luka Žaja <luka.zaja@protonmail.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later
""" refind-btrfs - Generate rEFInd manual boot stanzas from Btrfs snapshots
Copyright (C) 2020-2023 Luka Žaja
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
# endregion
from .device_command import DeviceCommand
from .icon_command import IconCommand
from .subvolume_command import SubvolumeCommand

Some files were not shown because too many files have changed in this diff Show More