Compare commits
12 Commits
4e780944ca
...
v1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fe574352c | ||
|
|
54f77ab854 | ||
|
|
7cb0af1660 | ||
|
|
2f3a079a43 | ||
|
|
25d0a7a678 | ||
|
|
a01d63fc9e | ||
|
|
c1020e964b | ||
|
|
67e09d3b0f | ||
|
|
c8b2172fd7 | ||
|
|
f67afa0586 | ||
|
|
e445ec248b | ||
|
|
4de9da0068 |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*.egg-info
|
||||||
|
dist/
|
||||||
9
CHANGELOG.md
Normal file
9
CHANGELOG.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## [1.0.1] - 2026-01-12
|
||||||
|
|
||||||
|
* Fixed build file ignores
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-01-12
|
||||||
|
|
||||||
|
* Official Release🥳
|
||||||
|
|
||||||
4
MIRRORS
Normal file
4
MIRRORS
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
git@github.com:kevinveenbirkenbach/setup-hibernate.git
|
||||||
|
ssh://git@git.veen.world:2201/kevinveenbirkenbach/setup-hibernate.git
|
||||||
|
ssh://git@code.infinito.nexus:2201/kevinveenbirkenbach/setup-hibernate.git
|
||||||
|
https://pypi.org/project/setup-hibernate/
|
||||||
136
README.md
136
README.md
@@ -1,2 +1,134 @@
|
|||||||
# hibernate
|
# Hibernate Setup
|
||||||
💤 A Python script to configure hibernation on Linux systems using a swap file, including GRUB and initramfs integration. Optional swap file creation with custom size support.
|
|
||||||
|
A Python-based utility for configuring hibernation on Linux systems using a swap file.
|
||||||
|
This tool automates resume configuration for GRUB and initramfs and optionally creates a swap file of configurable size.
|
||||||
|
|
||||||
|
Designed for Arch-based systems (Arch Linux, Manjaro) and intended for automation and reproducible system setup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 Features
|
||||||
|
|
||||||
|
* Optionally create and activate a swap file
|
||||||
|
* Automatically detect UUID and resume offset
|
||||||
|
* Inject `resume` and `resume_offset` into GRUB config
|
||||||
|
* Regenerate initramfs via `mkinitcpio`
|
||||||
|
* Interactive confirmation before file changes
|
||||||
|
* Preview mode for dry-run without changes
|
||||||
|
* Non-interactive mode for automation
|
||||||
|
* Root permission required
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Installation
|
||||||
|
|
||||||
|
### Using pip (recommended)
|
||||||
|
|
||||||
|
Install directly from PyPI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install setup-hibernate
|
||||||
|
```
|
||||||
|
|
||||||
|
Or system-wide:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pip install setup-hibernate
|
||||||
|
```
|
||||||
|
|
||||||
|
Or isolated using pipx:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pipx install setup-hibernate
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### From source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/kevinveenbirkenbach/setup-hibernate.git
|
||||||
|
cd setup-hibernate
|
||||||
|
pip install .
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Usage
|
||||||
|
|
||||||
|
Run the tool as root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo setup-hibernate [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via Python module:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo python -m setup_hibernate [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙ Options
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| ------------------- | -------------------------------------------------------------------- |
|
||||||
|
| `--create-swapfile` | Create and configure a swap file at `/swapfile` |
|
||||||
|
| `--swap-size <int>` | Set the swap file size in GB (default: `32`) |
|
||||||
|
| `-p`, `--preview` | Show what would be done without executing any changes (dry-run mode) |
|
||||||
|
| `--non-interactive` | Apply all changes automatically without prompting for confirmation |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Examples
|
||||||
|
|
||||||
|
Create a 40GB swapfile and configure hibernation interactively:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo setup-hibernate --create-swapfile --swap-size 40
|
||||||
|
```
|
||||||
|
|
||||||
|
Preview what would happen without actually doing anything:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo setup-hibernate --create-swapfile --swap-size 40 --preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Non-interactive, suitable for automation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo setup-hibernate --create-swapfile --swap-size 40 --non-interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Requirements
|
||||||
|
|
||||||
|
* Python 3.8+
|
||||||
|
* Tools:
|
||||||
|
|
||||||
|
* `fallocate`
|
||||||
|
* `mkswap`
|
||||||
|
* `swapon`
|
||||||
|
* `filefrag`
|
||||||
|
* `findmnt`
|
||||||
|
* `mkinitcpio`
|
||||||
|
* `update-grub`
|
||||||
|
* Root privileges
|
||||||
|
|
||||||
|
No external Python packages are required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👤 Author
|
||||||
|
|
||||||
|
Developed by **Kevin Veen-Birkenbach**
|
||||||
|
🌐 [https://www.veen.world](https://www.veen.world)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the **MIT License**.
|
||||||
|
See [LICENSE](./LICENSE) for details.
|
||||||
|
|||||||
34
pyproject.toml
Normal file
34
pyproject.toml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=69", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "setup-hibernate"
|
||||||
|
version = "1.0.1"
|
||||||
|
description = "Configure Linux hibernation (swapfile, GRUB resume, mkinitcpio resume hook)."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
license = { file = "LICENSE" }
|
||||||
|
authors = [{ name = "Kevin Veen-Birkenbach" }]
|
||||||
|
keywords = ["hibernate", "hibernation", "swap", "grub", "mkinitcpio", "linux", "arch", "manjaro"]
|
||||||
|
classifiers = [
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
"Operating System :: POSIX :: Linux",
|
||||||
|
"Environment :: Console",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://www.veen.world"
|
||||||
|
Repository = "https://github.com/kevinveenbirkenbach/setup-hibernate"
|
||||||
|
Issues = "https://github.com/kevinveenbirkenbach/setup-hibernate/issues"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
setup-hibernate = "setup_hibernate.__main__:main"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
package-dir = { "" = "src" }
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
0
src/setup_hibernate/__init__.py
Normal file
0
src/setup_hibernate/__init__.py
Normal file
202
src/setup_hibernate/__main__.py
Executable file
202
src/setup_hibernate/__main__.py
Executable file
@@ -0,0 +1,202 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import argparse
|
||||||
|
import difflib
|
||||||
|
|
||||||
|
SWAPFILE = "/swapfile"
|
||||||
|
FSTAB = "/etc/fstab"
|
||||||
|
GRUB_CONF = "/etc/default/grub"
|
||||||
|
MKINITCPIO = "/etc/mkinitcpio.conf"
|
||||||
|
|
||||||
|
preview = False
|
||||||
|
non_interactive = False
|
||||||
|
|
||||||
|
def run(cmd, capture=False):
|
||||||
|
if preview:
|
||||||
|
print(f"[preview] Would run: {cmd}")
|
||||||
|
return "" if capture else None
|
||||||
|
subprocess.run(cmd, shell=True, check=True)
|
||||||
|
if capture:
|
||||||
|
return subprocess.check_output(cmd, shell=True, text=True).strip()
|
||||||
|
|
||||||
|
def confirm_file_change(path, new_lines):
|
||||||
|
if preview or non_interactive:
|
||||||
|
print(f"[preview] Would write changes to {path}")
|
||||||
|
return True
|
||||||
|
with open(path, "r") as f:
|
||||||
|
old_lines = f.readlines()
|
||||||
|
diff = list(difflib.unified_diff(old_lines, new_lines, fromfile=path, tofile=path))
|
||||||
|
if diff:
|
||||||
|
print("\n--- Diff ---")
|
||||||
|
for line in diff:
|
||||||
|
print(line.rstrip())
|
||||||
|
answer = input(f"[?] Apply these changes to {path}? (y/N): ")
|
||||||
|
return answer.lower() == "y"
|
||||||
|
else:
|
||||||
|
print(f"[-] No changes needed for {path}.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_swapfile(size_gb):
|
||||||
|
print(f"[+] Creating {size_gb}G swapfile...")
|
||||||
|
run(f"fallocate -l {size_gb}G {SWAPFILE}")
|
||||||
|
run(f"chmod 600 {SWAPFILE}")
|
||||||
|
run(f"mkswap {SWAPFILE}")
|
||||||
|
run(f"swapon {SWAPFILE}")
|
||||||
|
|
||||||
|
def update_fstab():
|
||||||
|
print("[+] Ensuring swapfile is in /etc/fstab...")
|
||||||
|
if preview:
|
||||||
|
print(f"[preview] Would append to {FSTAB}: {SWAPFILE} none swap defaults 0 0")
|
||||||
|
return
|
||||||
|
with open(FSTAB, "r") as f:
|
||||||
|
old_lines = f.readlines()
|
||||||
|
if any(SWAPFILE in line for line in old_lines):
|
||||||
|
print("[-] Swapfile already in fstab.")
|
||||||
|
return
|
||||||
|
new_lines = old_lines + [f"{SWAPFILE} none swap defaults 0 0\n"]
|
||||||
|
if confirm_file_change(FSTAB, new_lines):
|
||||||
|
with open(FSTAB, "w") as f:
|
||||||
|
f.writelines(new_lines)
|
||||||
|
else:
|
||||||
|
print("[!] Skipped writing to fstab.")
|
||||||
|
|
||||||
|
def get_swap_uuid():
|
||||||
|
print("[+] Getting swap UUID...")
|
||||||
|
return run(f"findmnt -no UUID -T {SWAPFILE}", capture=True)
|
||||||
|
|
||||||
|
def get_resume_offset():
|
||||||
|
print("[+] Calculating resume_offset...")
|
||||||
|
out = run(f"filefrag -v {SWAPFILE}", capture=True)
|
||||||
|
for line in out.splitlines():
|
||||||
|
match = re.search(r"^\s*\d+:\s+\d+\.\.\s*\d+:\s+(\d+)", line)
|
||||||
|
if match:
|
||||||
|
offset = match.group(1)
|
||||||
|
if offset != "0":
|
||||||
|
print(f"[✓] Found resume_offset: {offset}")
|
||||||
|
return offset
|
||||||
|
raise RuntimeError("Couldn't find valid resume offset.")
|
||||||
|
|
||||||
|
def update_grub(uuid, offset):
|
||||||
|
print("[+] Updating GRUB_CMDLINE_LINUX_DEFAULT...")
|
||||||
|
if preview:
|
||||||
|
print(f"[preview] Would modify {GRUB_CONF} to include resume=UUID={uuid} resume_offset={offset}")
|
||||||
|
run("update-grub")
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(GRUB_CONF, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
new_lines = lines[:]
|
||||||
|
for i, line in enumerate(new_lines):
|
||||||
|
if line.startswith("GRUB_CMDLINE_LINUX_DEFAULT"):
|
||||||
|
# Extract current quoted content
|
||||||
|
match = re.match(r'^(GRUB_CMDLINE_LINUX_DEFAULT=)(["\'])(.*)(\2)$', line.strip())
|
||||||
|
if not match:
|
||||||
|
print("[!] Unexpected format in GRUB_CMDLINE_LINUX_DEFAULT, skipping safe modification.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
prefix, quote, content, _ = match.groups()
|
||||||
|
|
||||||
|
# Remove existing resume/resume_offset if present
|
||||||
|
content = re.sub(r'\s*resume=UUID=\S+', '', content)
|
||||||
|
content = re.sub(r'\s*resume_offset=\S+', '', content)
|
||||||
|
content = content.strip()
|
||||||
|
|
||||||
|
# Append new values
|
||||||
|
resume_args = f"resume=UUID={uuid} resume_offset={offset}"
|
||||||
|
if content:
|
||||||
|
content += f" {resume_args}"
|
||||||
|
else:
|
||||||
|
content = resume_args
|
||||||
|
|
||||||
|
# Write the modified line
|
||||||
|
new_lines[i] = f"{prefix}{quote}{content}{quote}\n"
|
||||||
|
break
|
||||||
|
|
||||||
|
if confirm_file_change(GRUB_CONF, new_lines):
|
||||||
|
with open(GRUB_CONF, "w") as f:
|
||||||
|
f.writelines(new_lines)
|
||||||
|
run("update-grub")
|
||||||
|
else:
|
||||||
|
print("[!] Skipped writing to grub config.")
|
||||||
|
|
||||||
|
def update_mkinitcpio():
|
||||||
|
print("[+] Ensuring resume hook in mkinitcpio.conf...")
|
||||||
|
if preview:
|
||||||
|
print(f"[preview] Would ensure 'resume' is included in {MKINITCPIO}")
|
||||||
|
run("mkinitcpio -P")
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(MKINITCPIO, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
new_lines = lines[:]
|
||||||
|
for i, line in enumerate(new_lines):
|
||||||
|
if line.startswith("HOOKS="):
|
||||||
|
# Extract hook list
|
||||||
|
match = re.search(r'\((.*?)\)', line)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
|
||||||
|
hooks = match.group(1).split()
|
||||||
|
if "resume" in hooks:
|
||||||
|
print("[-] 'resume' hook already present.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Insert resume after encrypt (or just before filesystems as fallback)
|
||||||
|
if "encrypt" in hooks:
|
||||||
|
index = hooks.index("encrypt") + 1
|
||||||
|
elif "filesystems" in hooks:
|
||||||
|
index = hooks.index("filesystems")
|
||||||
|
else:
|
||||||
|
index = len(hooks)
|
||||||
|
|
||||||
|
hooks.insert(index, "resume")
|
||||||
|
new_line = f'HOOKS=({" ".join(hooks)})\n'
|
||||||
|
new_lines[i] = new_line
|
||||||
|
break
|
||||||
|
|
||||||
|
if confirm_file_change(MKINITCPIO, new_lines):
|
||||||
|
with open(MKINITCPIO, "w") as f:
|
||||||
|
f.writelines(new_lines)
|
||||||
|
run("mkinitcpio -P")
|
||||||
|
else:
|
||||||
|
print("[!] Skipped writing to mkinitcpio.")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global preview, non_interactive
|
||||||
|
|
||||||
|
if os.geteuid() != 0:
|
||||||
|
print("This script must be run as root.")
|
||||||
|
return
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Configure hibernation with optional swapfile setup.")
|
||||||
|
parser.add_argument("--create-swapfile", action="store_true", help="Create and configure a swapfile")
|
||||||
|
parser.add_argument("--swap-size", type=int, default=32, help="Swapfile size in GB (default: 32)")
|
||||||
|
parser.add_argument("-p", "--preview", action="store_true", help="Show what would be done without making changes")
|
||||||
|
parser.add_argument("--non-interactive", action="store_true", help="Apply all changes without prompting")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
preview = args.preview
|
||||||
|
non_interactive = args.non_interactive
|
||||||
|
|
||||||
|
if args.create_swapfile:
|
||||||
|
create_swapfile(args.swap_size)
|
||||||
|
update_fstab()
|
||||||
|
|
||||||
|
uuid = get_swap_uuid()
|
||||||
|
offset = get_resume_offset()
|
||||||
|
update_grub(uuid, offset)
|
||||||
|
update_mkinitcpio()
|
||||||
|
|
||||||
|
if preview:
|
||||||
|
print("\n✅ Hibernate setup preview complete.")
|
||||||
|
else:
|
||||||
|
print("\n✅ Hibernate setup complete. Please reboot your system:")
|
||||||
|
print(" sudo reboot")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user