fix
This commit is contained in:
parent
99cda286f2
commit
5294c5eabc
2
debian/changelog
vendored
2
debian/changelog
vendored
@ -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
|
||||
|
||||
|
3
main.sh
3
main.sh
@ -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
|
||||
|
||||
|
215
refind-btrfs/etc/refind-btrfs.conf
Normal file
215
refind-btrfs/etc/refind-btrfs.conf
Normal 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"
|
2
refind-btrfs/usr/bin/refind-btrfs
Executable file
2
refind-btrfs/usr/bin/refind-btrfs
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
python -m refind_btrfs --run-mode one-time
|
@ -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.
|
@ -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
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,2 @@
|
||||
[console_scripts]
|
||||
refind-btrfs = refind_btrfs:main
|
@ -0,0 +1,13 @@
|
||||
antlr4-python3-runtime
|
||||
injector
|
||||
more-itertools
|
||||
pid
|
||||
semantic-version
|
||||
systemd-python
|
||||
tomlkit
|
||||
transitions
|
||||
typeguard
|
||||
watchdog
|
||||
|
||||
[custom_icon]
|
||||
Pillow
|
@ -0,0 +1 @@
|
||||
refind_btrfs
|
@ -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
|
@ -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())
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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
|
@ -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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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]
|
@ -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
|
@ -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))
|
@ -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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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!"
|
||||
)
|
@ -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),
|
||||
)
|
@ -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
|
@ -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]
|
@ -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
|
@ -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)
|
@ -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()
|
@ -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())
|
@ -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,
|
||||
)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
@ -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
|
@ -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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user