public-ready-init
This commit is contained in:
commit
6e8d1c9392
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
14
.idea/gentoo-legion-python.iml
generated
Normal file
14
.idea/gentoo-legion-python.iml
generated
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/gentoo-legion-python.iml" filepath="$PROJECT_DIR$/.idea/gentoo-legion-python.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
130
.zshrc
Normal file
130
.zshrc
Normal file
@ -0,0 +1,130 @@
|
||||
# ~/.zshrc
|
||||
|
||||
export TERM="xterm-256color"
|
||||
|
||||
# --- Basic Zsh Configuration ---
|
||||
|
||||
# Set default editor (for consistency)
|
||||
export EDITOR="nano" # Or "vim", "nvim", etc.
|
||||
|
||||
# History settings
|
||||
HISTFILE=~/.zsh_history
|
||||
SAVEHIST=10000 # Number of history entries to save
|
||||
HISTSIZE=10000 # Number of history entries to keep in memory
|
||||
setopt appendhistory # Append history to the history file
|
||||
setopt sharehistory # Share history among all sessions
|
||||
setopt hist_ignore_dups # Ignore duplicate commands
|
||||
setopt hist_verify # Ask for confirmation before executing history expansion
|
||||
|
||||
# Autocompletion
|
||||
autoload -U compinit
|
||||
compinit
|
||||
|
||||
# Better completion styling
|
||||
zstyle ':completion:*' menu select
|
||||
zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' 'r:|[._-]=* r:|=*' 'l:|=* r:|=*'
|
||||
zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}"
|
||||
zstyle ':completion:*' group-name ''
|
||||
zstyle ':completion:*:descriptions' format '%F{yellow}-- %d --%f'
|
||||
zstyle ':completion:*:warnings' format '%F{red}-- no matches found --%f'
|
||||
|
||||
# Zsh options
|
||||
setopt autocd # Change directory just by typing directory name
|
||||
setopt extendedglob # Enable extended globbing (e.g., removal of multiple files)
|
||||
setopt no_beep # Disable the bell
|
||||
setopt correct # Correct common typos
|
||||
setopt complete_in_word # Complete from the middle of a word
|
||||
|
||||
# Load any profile environment variables
|
||||
if [[ -f /etc/zsh/zprofile ]]; then
|
||||
source /etc/zsh/zprofile
|
||||
fi
|
||||
if [[ -f /etc/zsh/zshrc ]]; then
|
||||
source /etc/zsh/zshrc
|
||||
fi
|
||||
|
||||
# --- fzf Integration ---
|
||||
|
||||
# Load fzf key bindings and completions.
|
||||
# These files are installed by app-shells/fzf
|
||||
if [[ -f "/usr/share/fzf/key-bindings.zsh" ]]; then
|
||||
source "/usr/share/fzf/key-bindings.zsh"
|
||||
fi
|
||||
if [[ -f "/usr/share/fzf/completion.zsh" ]]; then
|
||||
source "/usr/share/fzf/completion.zsh"
|
||||
fi
|
||||
|
||||
# --- Aliases ---
|
||||
|
||||
# General aliases
|
||||
alias c='clear'
|
||||
alias df='df -h'
|
||||
alias du='du -sh'
|
||||
alias ip='ip -c a'
|
||||
alias ping='ping -c 5'
|
||||
alias top='htop' # Assuming htop is installed (it's in your list)
|
||||
alias nano='nano -c' # Show cursor position
|
||||
|
||||
# Git aliases
|
||||
alias g='git'
|
||||
alias gs='git status'
|
||||
alias ga='git add .'
|
||||
alias gc='git commit -m'
|
||||
alias gp='git push'
|
||||
alias gl='git log --oneline --decorate --all --graph'
|
||||
|
||||
# --- lsd Aliases ---
|
||||
# Replace ls with lsd for better visuals
|
||||
alias ls='lsd'
|
||||
alias l='lsd -F' # List only files, no directories
|
||||
alias ll='lsd -l' # Long format
|
||||
alias la='lsd -a' # All files
|
||||
alias lld='lsd -ld' # Long format, directories only
|
||||
alias lla='lsd -la' # Long format, all files
|
||||
alias lt='lsd --tree' # Tree view
|
||||
|
||||
# Wireguard VPN control function
|
||||
vpn() {
|
||||
case "$1" in
|
||||
up)
|
||||
sudo wg-quick up nexus
|
||||
;;
|
||||
down)
|
||||
sudo wg-quick down nexus
|
||||
;;
|
||||
status)
|
||||
sudo wg show nexus 2>/dev/null || echo "VPN is down"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: vpn {up|down|status}"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Neovide wrapper - automatically backgrounds the process and closes terminal
|
||||
neovide() {
|
||||
command neovide "$@" &>/dev/null &
|
||||
disown
|
||||
exit
|
||||
}
|
||||
|
||||
# Initialize starship prompt
|
||||
eval "$(starship init zsh)"
|
||||
|
||||
# Initialize zoxide (smarter cd)
|
||||
eval "$(zoxide init zsh)"
|
||||
|
||||
export PATH=$PATH:$HOME/.local/bin
|
||||
|
||||
# Autosuggestions from history (fish-style)
|
||||
source /usr/share/zsh/site-functions/zsh-autosuggestions.zsh
|
||||
ZSH_AUTOSUGGEST_STRATEGY=(history completion)
|
||||
ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=8'
|
||||
|
||||
# Ctrl+→ accepts one word from autosuggestion
|
||||
bindkey '^[[1;5C' forward-word
|
||||
|
||||
# Syntax highlighting (must be at the end)
|
||||
source /usr/share/zsh/site-functions/zsh-syntax-highlighting.zsh
|
||||
[ -s "/home/damien/.jabba/jabba.sh" ] && source "/home/damien/.jabba/jabba.sh"
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
73
README.md
Normal file
73
README.md
Normal file
@ -0,0 +1,73 @@
|
||||
# Gentoo Legion Installer
|
||||
|
||||
Automated Gentoo Linux installer for Lenovo Legion laptops with AMD + NVIDIA hybrid graphics.
|
||||
|
||||
## Features
|
||||
|
||||
- **Python-based installer** with idempotent operations (safe to re-run)
|
||||
- **LUKS2 encryption** with Btrfs subvolumes
|
||||
- **Hybrid GPU support** (AMD iGPU + NVIDIA dGPU)
|
||||
- **OpenRC** init system
|
||||
- **Hyprland** desktop environment (optional)
|
||||
- **Fingerprint authentication** for Elan readers (optional)
|
||||
|
||||
## Hardware
|
||||
|
||||
Tested on Legion S7 15ACH6:
|
||||
- AMD Ryzen 9 5900HX
|
||||
- AMD Radeon Vega (iGPU)
|
||||
- NVIDIA GeForce RTX 3050 Ti Mobile (dGPU)
|
||||
|
||||
Should work on other Legion models with similar hardware.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Boot Gentoo live environment, then:
|
||||
git clone https://github.com/<your-username>/gentoo-legion-python /root/gentoo
|
||||
cd /root/gentoo
|
||||
python setup.py --install
|
||||
```
|
||||
|
||||
See [procedure.md](procedure.md) for complete installation guide.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── setup.py # Main installer entry point
|
||||
├── procedure.md # Complete installation guide
|
||||
├── install/ # Python installer modules
|
||||
│ ├── disk.py # LUKS + Btrfs setup
|
||||
│ ├── stage3.py # Stage3 download/extract
|
||||
│ ├── sync.py # Portage sync, profile, locale
|
||||
│ ├── services.py # OpenRC services setup
|
||||
│ ├── users.py # User creation, shell config
|
||||
│ ├── nvidia.py # NVIDIA driver setup
|
||||
│ ├── bootloader.py # GRUB installation
|
||||
│ └── fingerprint.py # Fingerprint auth (optional)
|
||||
├── portage/ # Portage configuration
|
||||
│ ├── make.conf
|
||||
│ ├── package.use/
|
||||
│ ├── package.accept_keywords/
|
||||
│ └── sets/ # @hyprland package set
|
||||
├── dracut.conf.d/ # Initramfs configs
|
||||
├── conf.d/ # OpenRC service configs
|
||||
├── iptables/ # Firewall rules
|
||||
├── hypr/ # Hyprland configs
|
||||
├── .zshrc # Shell configuration
|
||||
└── starship.toml # Prompt theme
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
Before running, you may want to customize:
|
||||
|
||||
1. **portage/make.conf** - CPU flags, mirrors, features
|
||||
2. **install/sync.py** - Timezone, locale, hostname
|
||||
3. **install/disk.py** - Swap size, partition layout
|
||||
4. **iptables/*.rules** - Firewall rules for your network
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See [LICENSE](LICENSE)
|
||||
6
conf.d/dmcrypt
Normal file
6
conf.d/dmcrypt
Normal file
@ -0,0 +1,6 @@
|
||||
# /etc/conf.d/dmcrypt - Encrypted swap configuration
|
||||
|
||||
# Encrypted swap with random key (no hibernate support)
|
||||
# Key is regenerated each boot from /dev/urandom
|
||||
swap=cryptswap
|
||||
source="/dev/nvme0n1p2"
|
||||
7
conf.d/ip6tables
Normal file
7
conf.d/ip6tables
Normal file
@ -0,0 +1,7 @@
|
||||
# /etc/conf.d/ip6tables
|
||||
|
||||
# Don't save state on service stop (use static rules file)
|
||||
SAVE_ON_STOP="no"
|
||||
|
||||
# Rules file location
|
||||
IP6TABLES_SAVE="/etc/iptables/ip6tables.rules"
|
||||
7
conf.d/iptables
Normal file
7
conf.d/iptables
Normal file
@ -0,0 +1,7 @@
|
||||
# /etc/conf.d/iptables
|
||||
|
||||
# Don't save state on service stop (use static rules file)
|
||||
SAVE_ON_STOP="no"
|
||||
|
||||
# Rules file location
|
||||
IPTABLES_SAVE="/etc/iptables/iptables.rules"
|
||||
4
conf.d/snapper
Normal file
4
conf.d/snapper
Normal file
@ -0,0 +1,4 @@
|
||||
# /etc/conf.d/snapper - Snapper configuration
|
||||
# List of snapper configs to manage with hourly cron job
|
||||
|
||||
SNAPPER_CONFIGS="root"
|
||||
2
dracut.conf.d/crypt.conf
Normal file
2
dracut.conf.d/crypt.conf
Normal file
@ -0,0 +1,2 @@
|
||||
# LUKS support for encrypted root
|
||||
add_dracutmodules+=" crypt "
|
||||
4
dracut.conf.d/nvidia.conf
Normal file
4
dracut.conf.d/nvidia.conf
Normal file
@ -0,0 +1,4 @@
|
||||
# NVIDIA early KMS for Plymouth
|
||||
# Loads NVIDIA modules in initramfs for seamless graphical boot
|
||||
add_drivers+=" nvidia nvidia_modeset nvidia_uvm nvidia_drm "
|
||||
force_drivers+=" nvidia nvidia_modeset nvidia_uvm nvidia_drm "
|
||||
55
hypr/ENVariables.conf
Normal file
55
hypr/ENVariables.conf
Normal file
@ -0,0 +1,55 @@
|
||||
# /* ---- 💫 https://github.com/JaKooLit 💫 ---- */ #
|
||||
# Environment variables. See https://wiki.hyprland.org/Configuring/Environment-variables/
|
||||
|
||||
# Set your defaults editor through ENV in ~/.config/hypr/UserConfigs/01-UserDefaults.conf
|
||||
|
||||
### QT Variables ###
|
||||
# env = QT_AUTO_SCREEN_SCALE_FACTOR,1
|
||||
# env = QT_WAYLAND_DISABLE_WINDOWDECORATION,1
|
||||
# env = QT_QPA_PLATFORMTHEME,qt5ct
|
||||
# env = QT_QPA_PLATFORMTHEME,qt6ct
|
||||
|
||||
### xwayland apps scale fix (useful if you are use monitor scaling). ###
|
||||
# Set same value if you use scaling in Monitors.conf
|
||||
# 1 is 100% 1.5 is 150%
|
||||
# see https://wiki.hyprland.org/Configuring/XWayland/
|
||||
# env = GDK_SCALE,1
|
||||
# env = QT_SCALE_FACTOR,1
|
||||
|
||||
### NVIDIA ###
|
||||
# This is from Hyprland Wiki. Below will be activated nvidia gpu detected
|
||||
# See hyprland wiki https://wiki.hyprland.org/Nvidia/#environment-variables
|
||||
|
||||
#env = LIBVA_DRIVER_NAME,nvidia
|
||||
#env = __GLX_VENDOR_LIBRARY_NAME,nvidia
|
||||
#env = NVD_BACKEND,direct
|
||||
#env = GSK_RENDERER,ngl
|
||||
|
||||
### additional ENV's for nvidia. Caution, activate with care ###
|
||||
#env = GBM_BACKEND,nvidia-drm
|
||||
#env = __GL_GSYNC_ALLOWED,1 #adaptive Vsync
|
||||
#env = __NV_PRIME_RENDER_OFFLOAD,1
|
||||
#env = __VK_LAYER_NV_optimus,NVIDIA_only
|
||||
#env = WLR_DRM_NO_ATOMIC,1
|
||||
|
||||
### FOR VM and POSSIBLY NVIDIA ###
|
||||
# LIBGL_ALWAYS_SOFTWARE software mesa rendering
|
||||
#env = LIBGL_ALWAYS_SOFTWARE,1 # Warning. May cause hyprland to crash
|
||||
#env = WLR_RENDERER_ALLOW_SOFTWARE,1
|
||||
|
||||
### nvidia firefox ###
|
||||
# check this post https://github.com/elFarto/nvidia-vaapi-driver#configuration
|
||||
#env = MOZ_DISABLE_RDD_SANDBOX,1
|
||||
#env = EGL_PLATFORM,wayland
|
||||
|
||||
### Aquamarine Environment Variables (Hyprland > 0.45) ###
|
||||
# https://wiki.hyprland.org/Configuring/Environment-variables/#aquamarine-environment-variables
|
||||
# env = AQ_TRACE,1 # Enables more verbose logging.
|
||||
env = AQ_DRM_DEVICES,/dev/dri/card1:/dev/dri/card0 # AMD primary, NVIDIA for external displays
|
||||
env = AQ_MGPU_NO_EXPLICIT,1 # Disables explicit syncing on mgpu buffers (fixes pink screen)
|
||||
# env = AQ_NO_MODIFIERS,1 # Disables modifiers for DRM buffers
|
||||
|
||||
### Cursor ###
|
||||
env = XCURSOR_THEME,Bibata-Modern-Ice
|
||||
env = XCURSOR_SIZE,24
|
||||
|
||||
57
hypr/monitors.conf
Normal file
57
hypr/monitors.conf
Normal file
@ -0,0 +1,57 @@
|
||||
# /* ---- 💫 https://github.com/JaKooLit 💫 ---- */ #
|
||||
# default Monitor config
|
||||
|
||||
# *********************************************************** #
|
||||
#
|
||||
# NOTE: This will be overwritten by NWG-Displays
|
||||
# once you use and click apply. You can still find this
|
||||
# default at ~/.config/hypr/Monitor_Profiles/default.conf
|
||||
#
|
||||
# *********************************************************** #
|
||||
|
||||
|
||||
# Monitor Configuration
|
||||
# See Hyprland wiki for more details
|
||||
# https://wiki.hyprland.org/Configuring/Monitors/
|
||||
# Configure your Display resolution, offset, scale and Monitors here, use `hyprctl monitors` to get the info.
|
||||
|
||||
# Triple monitor layout: DP-1 (left) | eDP-2 (center/laptop) | DP-2 (right)
|
||||
monitor=DP-1, 1920x1080@60, 0x0, 1
|
||||
monitor=eDP-2, 1920x1080@60, 1920x0, 1
|
||||
monitor=DP-2, 1920x1080@60, 3840x0, 1
|
||||
|
||||
# NOTE: for laptop, kindly check notes in Laptops.conf regarding display
|
||||
# Created this inorder for the monitor display to not wake up if not intended.
|
||||
# See here: https://github.com/hyprwm/Hyprland/issues/4090
|
||||
|
||||
# Some examples to set your own monitor
|
||||
#monitor = eDP-1, preferred, auto, 1
|
||||
#monitor = eDP-1, 2560x1440@165, 0x0, 1 #own screen
|
||||
#monitor = DP-3, 1920x1080@240, auto, 1
|
||||
#monitor = DP-1, preferred, auto, 1
|
||||
#monitor = HDMI-A-1, preferred,auto,1
|
||||
|
||||
# QEMU-KVM, virtual box or vmware
|
||||
#monitor = Virtual-1, 1920x1080@60,auto,1
|
||||
|
||||
# to disable a monitor
|
||||
#monitor=name,disable
|
||||
|
||||
# Mirror samples
|
||||
#monitor=DP-3,1920x1080@60,0x0,1,mirror,DP-2
|
||||
#monitor=,preferred,auto,1,mirror,eDP-1
|
||||
#monitor=HDMI-A-1,2560x1440@144,0x0,1,mirror,eDP-1
|
||||
|
||||
# 10 bit monitor support - See wiki https://wiki.hyprland.org/Configuring/Monitors/#10-bit-support - See NOTES below
|
||||
# NOTE: Colors registered in Hyprland (e.g. the border color) do not support 10 bit.
|
||||
# NOTE: Some applications do not support screen capture with 10 bit enabled. (Screen captures like OBS may render black screen)
|
||||
# monitor=,preferred,auto,1,bitdepth,10
|
||||
|
||||
#monitor=eDP-1,transform,0
|
||||
#monitor=eDP-1,addreserved,10,10,10,49
|
||||
|
||||
# workspaces - Monitor rules
|
||||
# https://wiki.hyprland.org/Configuring/Workspace-Rules/
|
||||
# SUPER E - Workspace-Rules
|
||||
# See ~/.config/hypr/UserConfigs/WorkspaceRules.conf
|
||||
|
||||
30
hypr/wlogout-layout
Normal file
30
hypr/wlogout-layout
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"label" : "lock",
|
||||
"action" : "$HOME/.config/hypr/scripts/LockScreen.sh",
|
||||
"text" : "Lock",
|
||||
"keybind" : "l"
|
||||
}
|
||||
{
|
||||
"label" : "reboot",
|
||||
"action" : "loginctl reboot",
|
||||
"text" : "Reboot",
|
||||
"keybind" : "r"
|
||||
}
|
||||
{
|
||||
"label" : "shutdown",
|
||||
"action" : "loginctl poweroff",
|
||||
"text" : "Shutdown",
|
||||
"keybind" : "s"
|
||||
}
|
||||
{
|
||||
"label" : "logout",
|
||||
"action" : "hyprctl dispatch exit 0",
|
||||
"text" : "Logout",
|
||||
"keybind" : "e"
|
||||
}
|
||||
{
|
||||
"label" : "suspend",
|
||||
"action" : "loginctl suspend",
|
||||
"text" : "Suspend",
|
||||
"keybind" : "u"
|
||||
}
|
||||
3
install/__init__.py
Normal file
3
install/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""Gentoo installation automation for Legion S7 15ACH6."""
|
||||
|
||||
__version__ = "4.0.0"
|
||||
227
install/bootloader.py
Normal file
227
install/bootloader.py
Normal file
@ -0,0 +1,227 @@
|
||||
"""GRUB bootloader installation and configuration."""
|
||||
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from .utils import info, success, error, warn, run, emerge
|
||||
|
||||
|
||||
def emerge_grub() -> None:
|
||||
"""Install GRUB bootloader."""
|
||||
info("=== Installing GRUB ===")
|
||||
|
||||
emerge("sys-boot/grub")
|
||||
|
||||
success("GRUB installed.")
|
||||
|
||||
|
||||
def install_grub_efi() -> None:
|
||||
"""Install GRUB to EFI system partition."""
|
||||
info("=== Installing GRUB to EFI ===")
|
||||
|
||||
run(
|
||||
"grub-install",
|
||||
"--target=x86_64-efi",
|
||||
"--efi-directory=/boot",
|
||||
"--bootloader-id=Gentoo",
|
||||
)
|
||||
|
||||
success("GRUB installed to EFI partition.")
|
||||
|
||||
|
||||
def configure_grub() -> None:
|
||||
"""Configure GRUB for LUKS + NVIDIA."""
|
||||
info("=== Configuring GRUB ===")
|
||||
|
||||
grub_default = Path("/etc/default/grub")
|
||||
|
||||
if not grub_default.exists():
|
||||
error("/etc/default/grub not found")
|
||||
return
|
||||
|
||||
content = grub_default.read_text()
|
||||
changes_made = False
|
||||
|
||||
# Settings to apply (use regex for robust matching of commented/uncommented lines)
|
||||
settings = {
|
||||
"GRUB_CMDLINE_LINUX_DEFAULT": '"nvidia_drm.modeset=1 acpi_backlight=native"',
|
||||
"GRUB_ENABLE_CRYPTODISK": "y",
|
||||
"GRUB_GFXMODE": "1920x1080",
|
||||
}
|
||||
|
||||
for key, value in settings.items():
|
||||
full_setting = f'{key}={value}'
|
||||
pattern = rf'^#?\s*{key}=.*$'
|
||||
|
||||
if re.search(pattern, content, re.MULTILINE):
|
||||
# Replace existing line (commented or not)
|
||||
new_content = re.sub(pattern, full_setting, content, flags=re.MULTILINE)
|
||||
if new_content != content:
|
||||
content = new_content
|
||||
changes_made = True
|
||||
else:
|
||||
# Append if not found at all
|
||||
content += f"\n{full_setting}"
|
||||
changes_made = True
|
||||
|
||||
if changes_made:
|
||||
grub_default.write_text(content)
|
||||
success("GRUB configured for LUKS + NVIDIA + HiDPI")
|
||||
print(" - GRUB_CMDLINE_LINUX_DEFAULT: nvidia_drm.modeset=1 acpi_backlight=native")
|
||||
print(" - GRUB_ENABLE_CRYPTODISK: y")
|
||||
print(" - GRUB_GFXMODE: 1920x1080")
|
||||
else:
|
||||
info("GRUB already configured")
|
||||
|
||||
|
||||
def generate_grub_config() -> None:
|
||||
"""Generate GRUB configuration."""
|
||||
info("=== Generating GRUB Config ===")
|
||||
|
||||
run("grub-mkconfig", "-o", "/boot/grub/grub.cfg")
|
||||
|
||||
success("GRUB config generated.")
|
||||
|
||||
|
||||
def copy_dracut_crypt_config(source_dir: Path) -> None:
|
||||
"""Copy LUKS dracut configuration."""
|
||||
info("=== Copying Dracut Crypt Config ===")
|
||||
|
||||
dracut_src = source_dir / "dracut.conf.d" / "crypt.conf"
|
||||
dracut_dst = Path("/etc/dracut.conf.d")
|
||||
dracut_dst.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if dracut_src.exists():
|
||||
shutil.copy2(dracut_src, dracut_dst / "crypt.conf")
|
||||
success("Copied crypt.conf to /etc/dracut.conf.d/")
|
||||
else:
|
||||
# Create default if not in source
|
||||
crypt_conf = dracut_dst / "crypt.conf"
|
||||
crypt_conf.write_text(
|
||||
"# LUKS support for encrypted root\n"
|
||||
'add_dracutmodules+=" crypt "\n'
|
||||
)
|
||||
success("Created default /etc/dracut.conf.d/crypt.conf")
|
||||
|
||||
|
||||
def verify_initramfs() -> None:
|
||||
"""Verify initramfs has required modules for boot."""
|
||||
info("=== Verifying Initramfs ===")
|
||||
|
||||
# Find latest initramfs
|
||||
boot = Path("/boot")
|
||||
initramfs_files = sorted(boot.glob("initramfs-*.img"), reverse=True)
|
||||
|
||||
if not initramfs_files:
|
||||
error("No initramfs found in /boot")
|
||||
return
|
||||
|
||||
initramfs = initramfs_files[0]
|
||||
info(f"Checking {initramfs.name}...")
|
||||
|
||||
result = run("lsinitrd", str(initramfs), capture=True, check=False)
|
||||
|
||||
if not result.ok or not result.stdout:
|
||||
error("Could not inspect initramfs")
|
||||
return
|
||||
|
||||
output = result.stdout
|
||||
|
||||
# Check for crypt/LUKS modules
|
||||
print()
|
||||
info("LUKS/Crypt modules:")
|
||||
crypt_found = False
|
||||
for keyword in ["crypt", "luks", "dm-crypt"]:
|
||||
matches = [line for line in output.split("\n") if keyword in line.lower()]
|
||||
if matches:
|
||||
crypt_found = True
|
||||
for match in matches[:5]:
|
||||
print(f" {match}")
|
||||
|
||||
if crypt_found:
|
||||
success("LUKS support found in initramfs")
|
||||
else:
|
||||
error("WARNING: No LUKS/crypt modules found!")
|
||||
print(" Boot from encrypted root may fail.")
|
||||
|
||||
# Check for NVIDIA modules
|
||||
print()
|
||||
info("NVIDIA modules:")
|
||||
nvidia_found = False
|
||||
nvidia_modules = [line for line in output.split("\n") if "nvidia" in line.lower()]
|
||||
|
||||
if nvidia_modules:
|
||||
nvidia_found = True
|
||||
for module in nvidia_modules[:5]:
|
||||
print(f" {module}")
|
||||
success("NVIDIA modules found in initramfs")
|
||||
else:
|
||||
warn("No NVIDIA modules in initramfs (may be OK if not using early KMS)")
|
||||
|
||||
# Summary
|
||||
print()
|
||||
if crypt_found and nvidia_found:
|
||||
success("Initramfs verification PASSED - LUKS and NVIDIA modules present")
|
||||
elif crypt_found:
|
||||
success("Initramfs verification PASSED - system should boot (NVIDIA optional)")
|
||||
else:
|
||||
error("Initramfs verification FAILED - fix before rebooting!")
|
||||
|
||||
|
||||
def rebuild_initramfs() -> None:
|
||||
"""Rebuild initramfs with current configuration."""
|
||||
info("=== Rebuilding Initramfs ===")
|
||||
|
||||
# Find installed kernel version
|
||||
modules_dir = Path("/lib/modules")
|
||||
if not modules_dir.exists():
|
||||
error("No kernel modules found. Install kernel first.")
|
||||
return
|
||||
|
||||
kernels = sorted(modules_dir.iterdir(), reverse=True)
|
||||
if not kernels:
|
||||
error("No kernel versions found in /lib/modules")
|
||||
return
|
||||
|
||||
kernel_version = kernels[0].name
|
||||
info(f"Rebuilding initramfs for kernel {kernel_version}")
|
||||
|
||||
run("dracut", "--force", f"/boot/initramfs-{kernel_version}.img", kernel_version)
|
||||
|
||||
success("Initramfs rebuilt.")
|
||||
|
||||
|
||||
def setup_bootloader(source_dir: Path | None = None) -> None:
|
||||
"""Full bootloader setup workflow."""
|
||||
if source_dir is None:
|
||||
source_dir = Path("/root/gentoo")
|
||||
|
||||
# Ensure dracut crypt config is in place
|
||||
copy_dracut_crypt_config(source_dir)
|
||||
print()
|
||||
|
||||
emerge_grub()
|
||||
print()
|
||||
install_grub_efi()
|
||||
print()
|
||||
configure_grub()
|
||||
print()
|
||||
generate_grub_config()
|
||||
print()
|
||||
|
||||
# Verify initramfs
|
||||
verify_initramfs()
|
||||
|
||||
print()
|
||||
success("=== Bootloader Setup Complete ===")
|
||||
print()
|
||||
info("Pre-reboot checklist:")
|
||||
print(" 1. Verify /etc/fstab is correct")
|
||||
print(" 2. Verify /etc/conf.d/dmcrypt has swap configured")
|
||||
print(" 3. Ensure root password is set")
|
||||
print(" 4. Ensure user account exists")
|
||||
print()
|
||||
info("Ready to reboot!")
|
||||
print(" exit # Leave chroot")
|
||||
print(" reboot # Restart system")
|
||||
81
install/chroot.py
Normal file
81
install/chroot.py
Normal file
@ -0,0 +1,81 @@
|
||||
"""Chroot environment preparation."""
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from .utils import info, success, warn, run, is_mounted
|
||||
|
||||
|
||||
def prepare_chroot(mount_root: Path | None = None) -> None:
|
||||
"""Prepare chroot environment with bind mounts."""
|
||||
if mount_root is None:
|
||||
mount_root = Path("/mnt/gentoo")
|
||||
|
||||
info("=== Preparing Chroot Environment ===")
|
||||
|
||||
# Check if already prepared (idempotency)
|
||||
if is_mounted(mount_root / "proc"):
|
||||
info("Chroot already prepared (skipping)")
|
||||
return
|
||||
|
||||
# Copy DNS configuration
|
||||
info("Copying DNS configuration...")
|
||||
resolv_src = Path("/etc/resolv.conf")
|
||||
resolv_dst = mount_root / "etc/resolv.conf"
|
||||
|
||||
if resolv_src.exists():
|
||||
# Use --dereference to copy the actual file if it's a symlink
|
||||
shutil.copy2(resolv_src.resolve(), resolv_dst)
|
||||
success("Copied resolv.conf")
|
||||
|
||||
# Bind mount pseudo-filesystems
|
||||
mounts = [
|
||||
("proc", "/proc", ["--rbind"]),
|
||||
("sys", "/sys", ["--rbind"]),
|
||||
("dev", "/dev", ["--rbind"]),
|
||||
("run", "/run", ["--rbind"]),
|
||||
]
|
||||
|
||||
for name, source, flags in mounts:
|
||||
target = mount_root / name.lstrip("/")
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
run("mount", *flags, source, str(target))
|
||||
run("mount", "--make-rslave", str(target))
|
||||
success(f"Mounted {source}")
|
||||
|
||||
print()
|
||||
success("=== Chroot environment ready ===")
|
||||
print()
|
||||
info("To enter chroot:")
|
||||
print(f" chroot {mount_root} /bin/bash")
|
||||
print(" source /etc/profile")
|
||||
print(" export PS1=\"(chroot) $PS1\"")
|
||||
|
||||
|
||||
def enter_chroot(mount_root: Path | None = None) -> None:
|
||||
"""Enter the chroot environment."""
|
||||
if mount_root is None:
|
||||
mount_root = Path("/mnt/gentoo")
|
||||
|
||||
import os
|
||||
os.execvp("chroot", ["chroot", str(mount_root), "/bin/bash", "--login"])
|
||||
|
||||
|
||||
def cleanup_chroot(mount_root: Path | None = None) -> None:
|
||||
"""Unmount chroot bind mounts."""
|
||||
if mount_root is None:
|
||||
mount_root = Path("/mnt/gentoo")
|
||||
|
||||
info("Cleaning up chroot mounts...")
|
||||
|
||||
# Unmount in reverse order
|
||||
for name in ["run", "dev", "sys", "proc"]:
|
||||
target = mount_root / name
|
||||
result = run("umount", "-R", str(target), check=False)
|
||||
if not result.ok:
|
||||
# Try lazy unmount as fallback
|
||||
warn(f"Normal unmount failed for {target}, trying lazy unmount...")
|
||||
run("umount", "-Rl", str(target), check=False)
|
||||
|
||||
success("Chroot mounts cleaned up.")
|
||||
384
install/disk.py
Normal file
384
install/disk.py
Normal file
@ -0,0 +1,384 @@
|
||||
"""Disk partitioning, LUKS encryption, and btrfs setup."""
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from .utils import (
|
||||
info,
|
||||
success,
|
||||
error,
|
||||
fatal,
|
||||
prompt,
|
||||
confirm,
|
||||
run,
|
||||
run_quiet,
|
||||
is_block_device,
|
||||
is_mounted,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiskConfig:
|
||||
"""Configuration for disk setup."""
|
||||
efi_size_gb: int = 1
|
||||
swap_size_gb: int = 24
|
||||
luks_label: str = "legion"
|
||||
btrfs_label: str = "legion"
|
||||
mapper_name: str = "legion"
|
||||
btrfs_opts: str = "noatime,compress=zstd"
|
||||
mount_root: Path = field(default_factory=lambda: Path("/mnt/gentoo"))
|
||||
|
||||
@property
|
||||
def mapper_path(self) -> Path:
|
||||
return Path(f"/dev/mapper/{self.mapper_name}")
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiskLayout:
|
||||
"""Resolved disk layout after disk selection."""
|
||||
device: Path
|
||||
part_efi: Path
|
||||
part_swap: Path
|
||||
part_root: Path
|
||||
|
||||
@classmethod
|
||||
def from_device(cls, device: Path) -> "DiskLayout":
|
||||
"""Create layout with correct partition naming for device type."""
|
||||
dev_str = str(device)
|
||||
|
||||
if "nvme" in dev_str or "mmcblk" in dev_str:
|
||||
return cls(
|
||||
device=device,
|
||||
part_efi=Path(f"{dev_str}p1"),
|
||||
part_swap=Path(f"{dev_str}p2"),
|
||||
part_root=Path(f"{dev_str}p3"),
|
||||
)
|
||||
else:
|
||||
return cls(
|
||||
device=device,
|
||||
part_efi=Path(f"{dev_str}1"),
|
||||
part_swap=Path(f"{dev_str}2"),
|
||||
part_root=Path(f"{dev_str}3"),
|
||||
)
|
||||
|
||||
|
||||
def _is_luks_device(device: Path) -> bool:
|
||||
"""Check if a device is a LUKS container."""
|
||||
result = run_quiet("cryptsetup", "isLuks", str(device), check=False)
|
||||
return result.ok
|
||||
|
||||
|
||||
def _is_luks_open(mapper_name: str) -> bool:
|
||||
"""Check if a LUKS container is already open."""
|
||||
return Path(f"/dev/mapper/{mapper_name}").exists()
|
||||
|
||||
|
||||
def list_disks() -> list[dict]:
|
||||
"""List available disks with metadata."""
|
||||
result = run_quiet("lsblk", "-d", "-b", "-n", "-o", "NAME,SIZE,MODEL,TYPE")
|
||||
disks = []
|
||||
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
parts = line.split(None, 3)
|
||||
if len(parts) >= 2 and parts[-1] == "disk":
|
||||
name = parts[0]
|
||||
size_bytes = int(parts[1])
|
||||
model = parts[2] if len(parts) > 2 else "Unknown"
|
||||
disks.append({
|
||||
"name": name,
|
||||
"path": Path(f"/dev/{name}"),
|
||||
"size_gb": size_bytes // (1024**3),
|
||||
"model": model,
|
||||
})
|
||||
|
||||
return disks
|
||||
|
||||
|
||||
def select_disk() -> DiskLayout:
|
||||
"""Interactive disk selection."""
|
||||
info("Available disks:")
|
||||
disks = list_disks()
|
||||
|
||||
for i, disk in enumerate(disks, 1):
|
||||
print(f" {i}) {disk['path']} - {disk['size_gb']} GB ({disk['model']})")
|
||||
|
||||
print()
|
||||
choice = prompt("Enter disk number: ")
|
||||
|
||||
try:
|
||||
index = int(choice) - 1
|
||||
if index < 0 or index >= len(disks):
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
fatal("Invalid selection")
|
||||
|
||||
selected = disks[index]
|
||||
layout = DiskLayout.from_device(selected["path"])
|
||||
|
||||
print()
|
||||
error(f"WARNING: This will DESTROY ALL DATA on {layout.device}")
|
||||
if not confirm("Proceed?"):
|
||||
fatal("Aborted by user")
|
||||
|
||||
return layout
|
||||
|
||||
|
||||
def wipe_disk(layout: DiskLayout, config: DiskConfig) -> None:
|
||||
"""Wipe disk signatures and close any existing LUKS/mounts."""
|
||||
info("Wiping disk signatures...")
|
||||
|
||||
# Unmount target
|
||||
run("umount", "-R", str(config.mount_root), check=False)
|
||||
|
||||
# Close LUKS if open
|
||||
run("cryptsetup", "close", config.mapper_name, check=False)
|
||||
|
||||
# Deactivate swap
|
||||
run("swapoff", str(layout.part_swap), check=False)
|
||||
|
||||
# Wipe signatures
|
||||
run("wipefs", "-af", str(layout.device), check=False)
|
||||
|
||||
# Zero first 10MB
|
||||
run(
|
||||
"dd", "if=/dev/zero", f"of={layout.device}",
|
||||
"bs=1M", "count=10", "status=none",
|
||||
check=False,
|
||||
)
|
||||
|
||||
run("partprobe", str(layout.device), check=False)
|
||||
run("sync")
|
||||
|
||||
success("Disk wiped.")
|
||||
|
||||
|
||||
def create_partitions(layout: DiskLayout, config: DiskConfig) -> None:
|
||||
"""Create GPT partition table with EFI, swap, and root partitions."""
|
||||
info("Creating GPT partition table...")
|
||||
|
||||
device = str(layout.device)
|
||||
|
||||
# Clear and create GPT
|
||||
run("sgdisk", "--clear", device)
|
||||
run("sgdisk", "--set-alignment=2048", device)
|
||||
|
||||
# EFI partition
|
||||
run(
|
||||
"sgdisk",
|
||||
"-n", f"1:0:+{config.efi_size_gb}G",
|
||||
"-t", "1:EF00",
|
||||
"-c", "1:EFI",
|
||||
device,
|
||||
)
|
||||
|
||||
# Swap partition
|
||||
run(
|
||||
"sgdisk",
|
||||
"-n", f"2:0:+{config.swap_size_gb}G",
|
||||
"-t", "2:8200",
|
||||
"-c", "2:Swap",
|
||||
device,
|
||||
)
|
||||
|
||||
# Root partition (LUKS)
|
||||
run(
|
||||
"sgdisk",
|
||||
"-n", "3:0:0",
|
||||
"-t", "3:8309",
|
||||
"-c", "3:LUKS",
|
||||
device,
|
||||
)
|
||||
|
||||
run("partprobe", device)
|
||||
time.sleep(2)
|
||||
|
||||
# Verify partitions exist
|
||||
for part in [layout.part_efi, layout.part_swap, layout.part_root]:
|
||||
_wait_for_partition(part, layout.device)
|
||||
|
||||
success("Partitions created.")
|
||||
run("sgdisk", "-p", device)
|
||||
|
||||
|
||||
def _wait_for_partition(part: Path, device: Path, timeout: int = 10) -> None:
|
||||
"""Wait for partition to appear."""
|
||||
for _ in range(timeout):
|
||||
if is_block_device(part):
|
||||
return
|
||||
time.sleep(1)
|
||||
run_quiet("partprobe", str(device), check=False)
|
||||
|
||||
fatal(f"Partition {part} not found after {timeout}s")
|
||||
|
||||
|
||||
def format_efi(layout: DiskLayout) -> None:
|
||||
"""Format EFI partition as FAT32."""
|
||||
info(f"Formatting EFI partition {layout.part_efi}...")
|
||||
run("mkfs.vfat", "-F", "32", str(layout.part_efi))
|
||||
success("EFI formatted.")
|
||||
|
||||
|
||||
def setup_luks(layout: DiskLayout, config: DiskConfig) -> None:
|
||||
"""Create and open LUKS2 container."""
|
||||
info(f"Setting up LUKS2 encryption on {layout.part_root}...")
|
||||
success(">>> Enter your LUKS passphrase (OnlyKey) <<<")
|
||||
|
||||
run(
|
||||
"cryptsetup", "luksFormat",
|
||||
"--type", "luks2",
|
||||
"--label", config.luks_label,
|
||||
str(layout.part_root),
|
||||
)
|
||||
|
||||
info("Opening LUKS container...")
|
||||
success(">>> Enter passphrase again to open <<<")
|
||||
|
||||
run("cryptsetup", "open", str(layout.part_root), config.mapper_name)
|
||||
|
||||
success(f"LUKS container opened at {config.mapper_path}")
|
||||
|
||||
|
||||
def create_btrfs(config: DiskConfig) -> None:
|
||||
"""Create btrfs filesystem on LUKS container."""
|
||||
info("Creating btrfs filesystem...")
|
||||
run("mkfs.btrfs", "-L", config.btrfs_label, str(config.mapper_path))
|
||||
success(f"Btrfs created with label '{config.btrfs_label}'.")
|
||||
|
||||
|
||||
def create_subvolumes(config: DiskConfig) -> None:
|
||||
"""Create btrfs subvolumes."""
|
||||
info("Creating btrfs subvolumes...")
|
||||
|
||||
mount_root = config.mount_root
|
||||
mapper = str(config.mapper_path)
|
||||
|
||||
mount_root.mkdir(parents=True, exist_ok=True)
|
||||
run("mount", mapper, str(mount_root))
|
||||
|
||||
subvolumes = ["@", "@home", "@var", "@log", "@snapshots"]
|
||||
created = []
|
||||
skipped = []
|
||||
|
||||
for subvol in subvolumes:
|
||||
subvol_path = mount_root / subvol
|
||||
if subvol_path.exists():
|
||||
skipped.append(subvol)
|
||||
else:
|
||||
run("btrfs", "subvolume", "create", str(subvol_path))
|
||||
created.append(subvol)
|
||||
|
||||
run("umount", str(mount_root))
|
||||
|
||||
if created:
|
||||
success(f"Subvolumes created: {', '.join(created)}")
|
||||
if skipped:
|
||||
info(f"Subvolumes already exist: {', '.join(skipped)}")
|
||||
|
||||
|
||||
def mount_filesystems(layout: DiskLayout, config: DiskConfig) -> None:
|
||||
"""Mount all filesystems for installation."""
|
||||
info("Mounting filesystems...")
|
||||
|
||||
mount_root = config.mount_root
|
||||
mapper = str(config.mapper_path)
|
||||
opts = config.btrfs_opts
|
||||
|
||||
# Check if already mounted
|
||||
if is_mounted(mount_root):
|
||||
info(f"{mount_root} already mounted")
|
||||
run("findmnt", "-R", str(mount_root))
|
||||
return
|
||||
|
||||
# Mount @ subvolume first
|
||||
mount_root.mkdir(parents=True, exist_ok=True)
|
||||
run("mount", "-o", f"{opts},subvol=@", mapper, str(mount_root))
|
||||
|
||||
# Create mount points
|
||||
for subdir in ["home", "var", "var/log", ".snapshots", "boot"]:
|
||||
(mount_root / subdir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Mount remaining subvolumes (skip if already mounted)
|
||||
mounts = [
|
||||
(f"{opts},subvol=@home", mount_root / "home"),
|
||||
(f"{opts},subvol=@var", mount_root / "var"),
|
||||
(f"{opts},subvol=@log", mount_root / "var/log"),
|
||||
(f"{opts},subvol=@snapshots", mount_root / ".snapshots"),
|
||||
]
|
||||
for mount_opts, target in mounts:
|
||||
if not is_mounted(target):
|
||||
run("mount", "-o", mount_opts, mapper, str(target))
|
||||
|
||||
# Mount EFI
|
||||
if not is_mounted(mount_root / "boot"):
|
||||
run("mount", str(layout.part_efi), str(mount_root / "boot"))
|
||||
|
||||
success("All filesystems mounted.")
|
||||
run("findmnt", "-R", str(mount_root))
|
||||
|
||||
|
||||
def prepare_disk(config: DiskConfig | None = None) -> tuple[DiskLayout, DiskConfig]:
|
||||
"""Full disk preparation workflow."""
|
||||
if config is None:
|
||||
config = DiskConfig()
|
||||
|
||||
print()
|
||||
info("=== Gentoo Disk Preparation ===")
|
||||
info(f"EFI: {config.efi_size_gb} GB")
|
||||
info(f"Swap: {config.swap_size_gb} GB (encrypted at boot with random key)")
|
||||
info("Root: Remaining space (LUKS2 + btrfs)")
|
||||
print()
|
||||
|
||||
layout = select_disk()
|
||||
|
||||
# Check for existing LUKS container - offer to reuse instead of reformatting
|
||||
if layout.part_root.exists() and _is_luks_device(layout.part_root):
|
||||
print()
|
||||
info("Existing LUKS container detected on root partition.")
|
||||
response = prompt("Reformat disk (destroys data) or reuse existing? [reformat/reuse]: ").strip().lower()
|
||||
|
||||
if response == "reuse":
|
||||
info("Reusing existing disk setup...")
|
||||
# Open LUKS if not already open
|
||||
if not _is_luks_open(config.mapper_name):
|
||||
info("Opening existing LUKS container...")
|
||||
success(">>> Enter LUKS passphrase <<<")
|
||||
run("cryptsetup", "open", str(layout.part_root), config.mapper_name)
|
||||
# Mount filesystems
|
||||
mount_filesystems(layout, config)
|
||||
_print_summary(layout, config)
|
||||
return layout, config
|
||||
elif response != "reformat":
|
||||
fatal("Aborted - enter 'reformat' or 'reuse'")
|
||||
|
||||
# Full disk setup
|
||||
wipe_disk(layout, config)
|
||||
create_partitions(layout, config)
|
||||
format_efi(layout)
|
||||
setup_luks(layout, config)
|
||||
create_btrfs(config)
|
||||
create_subvolumes(config)
|
||||
mount_filesystems(layout, config)
|
||||
|
||||
_print_summary(layout, config)
|
||||
return layout, config
|
||||
|
||||
|
||||
def _print_summary(layout: DiskLayout, config: DiskConfig) -> None:
|
||||
"""Print disk preparation summary."""
|
||||
print()
|
||||
success("=== Disk preparation complete ===")
|
||||
print()
|
||||
info("Summary:")
|
||||
print(f" EFI: {layout.part_efi} -> {config.mount_root}/boot")
|
||||
print(f" Swap: {layout.part_swap} (encrypted at boot)")
|
||||
print(f" LUKS: {layout.part_root} -> {config.mapper_path}")
|
||||
print(f" Btrfs: {config.mapper_path}")
|
||||
print()
|
||||
info("Subvolumes:")
|
||||
print(" @ -> /")
|
||||
print(" @home -> /home")
|
||||
print(" @var -> /var")
|
||||
print(" @log -> /var/log")
|
||||
print(" @snapshots -> /.snapshots")
|
||||
print()
|
||||
277
install/fingerprint.py
Normal file
277
install/fingerprint.py
Normal file
@ -0,0 +1,277 @@
|
||||
"""Fingerprint authentication setup for Elan 04f3:0c4b reader.
|
||||
|
||||
This module sets up fingerprint authentication using fprintd with the
|
||||
Lenovo TOD (Touch OEM Drivers) driver for the Elan fingerprint sensor.
|
||||
|
||||
Requires:
|
||||
- fprintd and libfprint packages
|
||||
- Lenovo TOD driver (libfprint-2-tod1-elan.so)
|
||||
- User account to enroll fingerprints for
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from .utils import info, success, warn, error, fatal, run, prompt
|
||||
|
||||
# Lenovo TOD driver download URL (Ubuntu 22.04 package, works on Gentoo)
|
||||
LENOVO_DRIVER_URL = "https://download.lenovo.com/pccbbs/mobiles/r1elf10w.zip"
|
||||
TOD_DRIVER_NAME = "libfprint-2-tod1-elan.so"
|
||||
TOD_INSTALL_DIR = Path("/usr/lib64/libfprint-2/tod-1")
|
||||
|
||||
# PAM configuration for fingerprint (password OR fingerprint)
|
||||
PAM_FINGERPRINT_LINES = """\
|
||||
# Fingerprint authentication - press Enter on empty password to use fingerprint
|
||||
auth [success=1 new_authtok_reqd=1 default=ignore] pam_unix.so try_first_pass likeauth nullok
|
||||
auth sufficient pam_fprintd.so
|
||||
"""
|
||||
|
||||
# PAM files to configure
|
||||
PAM_FILES = [
|
||||
"/etc/pam.d/sddm",
|
||||
"/etc/pam.d/hyprlock",
|
||||
]
|
||||
|
||||
|
||||
def check_elan_device() -> bool:
|
||||
"""Check if Elan fingerprint reader is present."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["cat", "/sys/bus/usb/devices/*/product"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
return "ELAN:Fingerprint" in result.stdout
|
||||
except (FileNotFoundError, OSError):
|
||||
return False
|
||||
|
||||
|
||||
def install_packages() -> None:
|
||||
"""Install fprintd and libfprint."""
|
||||
info("Installing fingerprint packages...")
|
||||
from .utils import emerge
|
||||
emerge("sys-auth/fprintd", "sys-auth/libfprint")
|
||||
success("Fingerprint packages installed.")
|
||||
|
||||
|
||||
def download_tod_driver() -> Path:
|
||||
"""Download Lenovo TOD driver and extract the .so file."""
|
||||
dest = TOD_INSTALL_DIR / TOD_DRIVER_NAME
|
||||
|
||||
# Check if already installed (idempotency)
|
||||
if dest.exists():
|
||||
info("TOD driver already installed")
|
||||
return dest
|
||||
|
||||
info(f"Downloading Lenovo TOD driver from {LENOVO_DRIVER_URL}...")
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmppath = Path(tmpdir)
|
||||
zip_path = tmppath / "driver.zip"
|
||||
|
||||
# Download
|
||||
urllib.request.urlretrieve(LENOVO_DRIVER_URL, zip_path)
|
||||
success("Downloaded driver package.")
|
||||
|
||||
# Extract
|
||||
info("Extracting driver...")
|
||||
with zipfile.ZipFile(zip_path, "r") as zf:
|
||||
zf.extractall(tmppath)
|
||||
|
||||
# Find the .so file (it's nested in the archive)
|
||||
so_files = list(tmppath.rglob(TOD_DRIVER_NAME))
|
||||
if not so_files:
|
||||
fatal(f"Could not find {TOD_DRIVER_NAME} in downloaded package")
|
||||
|
||||
# Copy to install location
|
||||
TOD_INSTALL_DIR.mkdir(parents=True, exist_ok=True)
|
||||
dest = TOD_INSTALL_DIR / TOD_DRIVER_NAME
|
||||
|
||||
import shutil
|
||||
shutil.copy2(so_files[0], dest)
|
||||
dest.chmod(0o755)
|
||||
|
||||
success(f"Installed TOD driver to {dest}")
|
||||
return dest
|
||||
|
||||
|
||||
def verify_device() -> bool:
|
||||
"""Verify fprintd can see the device."""
|
||||
info("Verifying fingerprint device...")
|
||||
result = subprocess.run(
|
||||
["fprintd-list", "root"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0 or "No devices available" in result.stderr:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def enroll_fingerprints(username: str) -> None:
|
||||
"""Enroll fingerprints for a user."""
|
||||
info(f"Enrolling fingerprints for user '{username}'...")
|
||||
|
||||
# Check if already enrolled (idempotency)
|
||||
result = subprocess.run(
|
||||
["fprintd-list", username],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0 and "right-index-finger" in result.stdout:
|
||||
info(f"Fingerprints already enrolled for {username}")
|
||||
response = prompt("Re-enroll fingerprints? [y/N]: ").strip().lower()
|
||||
if response not in ("y", "yes"):
|
||||
return
|
||||
|
||||
print()
|
||||
print("You will be prompted to swipe your finger multiple times.")
|
||||
print("Press Ctrl+C to skip enrollment (you can do this later).")
|
||||
print()
|
||||
|
||||
fingers = ["right-index-finger", "left-index-finger"]
|
||||
|
||||
for finger in fingers:
|
||||
response = prompt(f"Enroll {finger}? [Y/n]: ").strip().lower()
|
||||
if response in ("", "y", "yes"):
|
||||
try:
|
||||
run("fprintd-enroll", "-f", finger, username)
|
||||
success(f"Enrolled {finger}.")
|
||||
except KeyboardInterrupt:
|
||||
warn(f"Skipped {finger}.")
|
||||
except subprocess.CalledProcessError:
|
||||
warn(f"Failed to enroll {finger}.")
|
||||
else:
|
||||
info(f"Skipped {finger}.")
|
||||
|
||||
print()
|
||||
info("Testing fingerprint verification...")
|
||||
run("fprintd-verify", username, check=False)
|
||||
|
||||
|
||||
def configure_pam() -> None:
|
||||
"""Configure PAM files for fingerprint authentication."""
|
||||
info("Configuring PAM for fingerprint authentication...")
|
||||
|
||||
for pam_file in PAM_FILES:
|
||||
pam_path = Path(pam_file)
|
||||
if not pam_path.exists():
|
||||
warn(f"PAM file not found: {pam_file} (skipping)")
|
||||
continue
|
||||
|
||||
content = pam_path.read_text()
|
||||
|
||||
# Check if already configured
|
||||
if "pam_fprintd.so" in content:
|
||||
info(f"PAM already configured: {pam_file}")
|
||||
continue
|
||||
|
||||
# Find the first 'auth' line and insert before it
|
||||
lines = content.splitlines()
|
||||
new_lines = []
|
||||
inserted = False
|
||||
|
||||
for line in lines:
|
||||
if not inserted and line.strip().startswith("auth"):
|
||||
# Insert fingerprint config before first auth line
|
||||
new_lines.extend(PAM_FINGERPRINT_LINES.strip().splitlines())
|
||||
inserted = True
|
||||
new_lines.append(line)
|
||||
|
||||
if inserted:
|
||||
pam_path.write_text("\n".join(new_lines) + "\n")
|
||||
success(f"Configured {pam_file}")
|
||||
else:
|
||||
warn(f"No auth line found in {pam_file}")
|
||||
|
||||
|
||||
def configure_sudo() -> None:
|
||||
"""Optionally configure sudo for fingerprint."""
|
||||
response = prompt("Enable fingerprint for sudo? [y/N]: ").strip().lower()
|
||||
if response not in ("y", "yes"):
|
||||
info("Skipped sudo fingerprint configuration.")
|
||||
return
|
||||
|
||||
pam_path = Path("/etc/pam.d/sudo")
|
||||
if not pam_path.exists():
|
||||
warn("PAM file not found: /etc/pam.d/sudo")
|
||||
return
|
||||
|
||||
content = pam_path.read_text()
|
||||
if "pam_fprintd.so" in content:
|
||||
info("sudo PAM already configured for fingerprint.")
|
||||
return
|
||||
|
||||
lines = content.splitlines()
|
||||
new_lines = []
|
||||
inserted = False
|
||||
|
||||
for line in lines:
|
||||
if not inserted and line.strip().startswith("auth"):
|
||||
new_lines.extend(PAM_FINGERPRINT_LINES.strip().splitlines())
|
||||
inserted = True
|
||||
new_lines.append(line)
|
||||
|
||||
if inserted:
|
||||
pam_path.write_text("\n".join(new_lines) + "\n")
|
||||
success("Configured /etc/pam.d/sudo")
|
||||
|
||||
|
||||
def setup_fingerprint(source_dir: Path) -> None:
|
||||
"""Main fingerprint setup routine."""
|
||||
info("=== Fingerprint Authentication Setup ===")
|
||||
print()
|
||||
|
||||
# Check for hardware
|
||||
if not check_elan_device():
|
||||
fatal("Elan fingerprint reader not detected. Is the device connected?")
|
||||
|
||||
info("Detected Elan fingerprint reader (04f3:0c4b)")
|
||||
print()
|
||||
|
||||
# Install packages
|
||||
install_packages()
|
||||
print()
|
||||
|
||||
# Install TOD driver
|
||||
download_tod_driver()
|
||||
print()
|
||||
|
||||
# Verify device is recognized
|
||||
if not verify_device():
|
||||
error("fprintd cannot see the device after driver installation.")
|
||||
error("You may need to reboot and run this command again.")
|
||||
return
|
||||
success("Fingerprint device recognized by fprintd.")
|
||||
print()
|
||||
|
||||
# Get username for enrollment
|
||||
username = prompt("Username to enroll fingerprints for: ").strip()
|
||||
if not username:
|
||||
fatal("Username required for enrollment.")
|
||||
|
||||
# Enroll fingerprints
|
||||
enroll_fingerprints(username)
|
||||
print()
|
||||
|
||||
# Configure PAM
|
||||
configure_pam()
|
||||
print()
|
||||
|
||||
# Optionally configure sudo
|
||||
configure_sudo()
|
||||
print()
|
||||
|
||||
success("=== Fingerprint setup complete ===")
|
||||
print()
|
||||
print("Usage:")
|
||||
print(" - SDDM: Press Enter on empty password field to use fingerprint")
|
||||
print(" - hyprlock: Press Enter on empty password field to use fingerprint")
|
||||
print(" - Enroll more fingers: fprintd-enroll -f <finger> <username>")
|
||||
print(" - Test: fprintd-verify <username>")
|
||||
126
install/fstab.py
Normal file
126
install/fstab.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""Generate fstab and crypttab configuration files."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .utils import info, success, run_quiet
|
||||
from .disk import DiskLayout, DiskConfig
|
||||
|
||||
|
||||
def get_uuid(device: Path) -> str:
|
||||
"""Get UUID of a block device."""
|
||||
result = run_quiet("blkid", "-s", "UUID", "-o", "value", str(device))
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def get_luks_uuid(device: Path) -> str:
|
||||
"""Get UUID of a LUKS container."""
|
||||
return get_uuid(device)
|
||||
|
||||
|
||||
def generate_fstab(
|
||||
layout: DiskLayout,
|
||||
config: DiskConfig,
|
||||
mount_root: Path | None = None,
|
||||
) -> None:
|
||||
"""Generate /etc/fstab for the new system."""
|
||||
if mount_root is None:
|
||||
mount_root = config.mount_root
|
||||
|
||||
info("Generating /etc/fstab...")
|
||||
|
||||
efi_uuid = get_uuid(layout.part_efi)
|
||||
mapper = config.mapper_path
|
||||
opts = config.btrfs_opts
|
||||
|
||||
fstab_content = f"""\
|
||||
# /etc/fstab - Generated by install-installer v4
|
||||
# Legion S7 with LUKS encryption + Btrfs subvolumes
|
||||
|
||||
# EFI System Partition
|
||||
UUID={efi_uuid} /boot vfat noatime,defaults 0 2
|
||||
|
||||
# Encrypted swap - NOTE: This entry is NOT used by swapon directly.
|
||||
# Swap activation is handled by the OpenRC dmcrypt service (/etc/conf.d/dmcrypt)
|
||||
# which creates /dev/mapper/swap with a random key at boot.
|
||||
{layout.part_swap} none swap sw,cipher=aes-xts-plain64,size=256 0 0
|
||||
|
||||
# Btrfs on LUKS ({config.mapper_name})
|
||||
{mapper} / btrfs {opts},subvol=@ 0 0
|
||||
{mapper} /home btrfs {opts},subvol=@home 0 0
|
||||
{mapper} /var btrfs {opts},subvol=@var 0 0
|
||||
{mapper} /var/log btrfs {opts},subvol=@log 0 0
|
||||
{mapper} /.snapshots btrfs {opts},subvol=@snapshots 0 0
|
||||
"""
|
||||
|
||||
fstab_path = mount_root / "etc/fstab"
|
||||
fstab_path.write_text(fstab_content)
|
||||
|
||||
success(f"Generated {fstab_path}")
|
||||
|
||||
|
||||
def generate_crypttab(
|
||||
layout: DiskLayout,
|
||||
config: DiskConfig,
|
||||
mount_root: Path | None = None,
|
||||
) -> None:
|
||||
"""Generate /etc/crypttab for the new system."""
|
||||
if mount_root is None:
|
||||
mount_root = config.mount_root
|
||||
|
||||
info("Generating /etc/crypttab...")
|
||||
|
||||
luks_uuid = get_luks_uuid(layout.part_root)
|
||||
|
||||
crypttab_content = f"""\
|
||||
# /etc/crypttab - Generated by install-installer v4
|
||||
# LUKS root partition
|
||||
|
||||
{config.mapper_name} UUID={luks_uuid} none luks
|
||||
"""
|
||||
|
||||
crypttab_path = mount_root / "etc/crypttab"
|
||||
crypttab_path.write_text(crypttab_content)
|
||||
|
||||
success(f"Generated {crypttab_path}")
|
||||
|
||||
|
||||
def generate_dmcrypt_conf(
|
||||
layout: DiskLayout,
|
||||
mount_root: Path | None = None,
|
||||
) -> None:
|
||||
"""Generate /etc/conf.d/dmcrypt for OpenRC encrypted swap."""
|
||||
if mount_root is None:
|
||||
mount_root = Path("/mnt/gentoo")
|
||||
|
||||
info("Generating /etc/conf.d/dmcrypt...")
|
||||
|
||||
dmcrypt_content = f"""\
|
||||
# /etc/conf.d/dmcrypt - Generated by install-installer v4
|
||||
# Encrypted swap with random key (no hibernate support)
|
||||
|
||||
swap=swap
|
||||
source='{layout.part_swap}'
|
||||
"""
|
||||
|
||||
dmcrypt_path = mount_root / "etc/conf.d/dmcrypt"
|
||||
dmcrypt_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
dmcrypt_path.write_text(dmcrypt_content)
|
||||
|
||||
success(f"Generated {dmcrypt_path}")
|
||||
|
||||
|
||||
def generate_all(
|
||||
layout: DiskLayout,
|
||||
config: DiskConfig,
|
||||
mount_root: Path | None = None,
|
||||
) -> None:
|
||||
"""Generate all filesystem configuration files."""
|
||||
if mount_root is None:
|
||||
mount_root = config.mount_root
|
||||
|
||||
generate_fstab(layout, config, mount_root)
|
||||
generate_crypttab(layout, config, mount_root)
|
||||
generate_dmcrypt_conf(layout, mount_root)
|
||||
|
||||
print()
|
||||
success("=== Filesystem configuration complete ===")
|
||||
171
install/nvidia.py
Normal file
171
install/nvidia.py
Normal file
@ -0,0 +1,171 @@
|
||||
"""NVIDIA driver installation and configuration."""
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from .utils import info, success, error, run, emerge, prompt
|
||||
|
||||
|
||||
def emerge_nvidia() -> None:
|
||||
"""Install NVIDIA drivers."""
|
||||
info("=== Installing NVIDIA Drivers ===")
|
||||
|
||||
emerge("x11-drivers/nvidia-drivers")
|
||||
|
||||
success("NVIDIA drivers installed.")
|
||||
|
||||
|
||||
def blacklist_nouveau() -> None:
|
||||
"""Blacklist nouveau driver."""
|
||||
info("=== Blacklisting Nouveau ===")
|
||||
|
||||
modprobe_dir = Path("/etc/modprobe.d")
|
||||
blacklist_conf = modprobe_dir / "blacklist-nouveau.conf"
|
||||
|
||||
# Check if already configured (idempotency)
|
||||
if blacklist_conf.exists():
|
||||
info("Nouveau already blacklisted")
|
||||
return
|
||||
|
||||
modprobe_dir.mkdir(parents=True, exist_ok=True)
|
||||
blacklist_conf.write_text(
|
||||
"# Blacklist nouveau in favor of proprietary nvidia driver\n"
|
||||
"blacklist nouveau\n"
|
||||
"options nouveau modeset=0\n"
|
||||
)
|
||||
|
||||
success(f"Created {blacklist_conf}")
|
||||
|
||||
|
||||
def configure_nvidia_drm() -> None:
|
||||
"""Configure NVIDIA DRM modeset."""
|
||||
info("=== Configuring NVIDIA DRM ===")
|
||||
|
||||
modprobe_dir = Path("/etc/modprobe.d")
|
||||
nvidia_conf = modprobe_dir / "nvidia.conf"
|
||||
|
||||
# Check if already configured (idempotency)
|
||||
if nvidia_conf.exists():
|
||||
info("NVIDIA DRM already configured")
|
||||
return
|
||||
|
||||
modprobe_dir.mkdir(parents=True, exist_ok=True)
|
||||
nvidia_conf.write_text(
|
||||
"# Enable NVIDIA DRM kernel mode setting\n"
|
||||
"options nvidia_drm modeset=1 fbdev=1\n"
|
||||
)
|
||||
|
||||
success(f"Created {nvidia_conf}")
|
||||
|
||||
|
||||
def copy_dracut_config(source_dir: Path) -> None:
|
||||
"""Copy NVIDIA dracut configuration."""
|
||||
info("=== Copying Dracut NVIDIA Config ===")
|
||||
|
||||
dracut_src = source_dir / "dracut.conf.d" / "nvidia.conf"
|
||||
dracut_dst = Path("/etc/dracut.conf.d")
|
||||
dracut_dst.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if dracut_src.exists():
|
||||
shutil.copy2(dracut_src, dracut_dst / "nvidia.conf")
|
||||
success("Copied nvidia.conf to /etc/dracut.conf.d/")
|
||||
else:
|
||||
# Create default if not in source
|
||||
nvidia_conf = dracut_dst / "nvidia.conf"
|
||||
nvidia_conf.write_text(
|
||||
"# NVIDIA early KMS\n"
|
||||
'add_drivers+=" nvidia nvidia_modeset nvidia_uvm nvidia_drm "\n'
|
||||
'force_drivers+=" nvidia nvidia_modeset nvidia_uvm nvidia_drm "\n'
|
||||
)
|
||||
success("Created default /etc/dracut.conf.d/nvidia.conf")
|
||||
|
||||
|
||||
def rebuild_initramfs() -> None:
|
||||
"""Rebuild initramfs to include NVIDIA modules."""
|
||||
info("=== Rebuilding Initramfs ===")
|
||||
|
||||
# Find installed kernel version
|
||||
modules_dir = Path("/lib/modules")
|
||||
if not modules_dir.exists():
|
||||
error("No kernel modules found. Install kernel first.")
|
||||
return
|
||||
|
||||
kernels = sorted(modules_dir.iterdir(), reverse=True)
|
||||
if not kernels:
|
||||
error("No kernel versions found in /lib/modules")
|
||||
return
|
||||
|
||||
kernel_version = kernels[0].name
|
||||
info(f"Rebuilding initramfs for kernel {kernel_version}")
|
||||
|
||||
run("dracut", "--force", f"/boot/initramfs-{kernel_version}.img", kernel_version)
|
||||
|
||||
success("Initramfs rebuilt.")
|
||||
|
||||
|
||||
def verify_initramfs() -> None:
|
||||
"""Verify NVIDIA modules are in initramfs."""
|
||||
info("=== Verifying Initramfs ===")
|
||||
|
||||
# Find latest initramfs
|
||||
boot = Path("/boot")
|
||||
initramfs_files = sorted(boot.glob("initramfs-*.img"), reverse=True)
|
||||
|
||||
if not initramfs_files:
|
||||
error("No initramfs found in /boot")
|
||||
return
|
||||
|
||||
initramfs = initramfs_files[0]
|
||||
info(f"Checking {initramfs.name}...")
|
||||
|
||||
# Check for NVIDIA modules
|
||||
result = run("lsinitrd", str(initramfs), capture=True, check=False)
|
||||
|
||||
if result.ok and result.stdout:
|
||||
nvidia_modules = [
|
||||
line for line in result.stdout.split("\n")
|
||||
if "nvidia" in line.lower()
|
||||
]
|
||||
|
||||
if nvidia_modules:
|
||||
success("NVIDIA modules found in initramfs:")
|
||||
for module in nvidia_modules[:10]: # Show first 10
|
||||
print(f" {module}")
|
||||
else:
|
||||
error("WARNING: No NVIDIA modules found in initramfs!")
|
||||
print(" Initramfs may need to be rebuilt after kernel installation.")
|
||||
else:
|
||||
error("Could not inspect initramfs")
|
||||
|
||||
|
||||
def setup_nvidia(source_dir: Path | None = None) -> None:
|
||||
"""Full NVIDIA setup workflow."""
|
||||
if source_dir is None:
|
||||
source_dir = Path("/root/gentoo")
|
||||
|
||||
emerge_nvidia()
|
||||
print()
|
||||
blacklist_nouveau()
|
||||
print()
|
||||
configure_nvidia_drm()
|
||||
print()
|
||||
copy_dracut_config(source_dir)
|
||||
print()
|
||||
|
||||
# Ask about rebuilding initramfs
|
||||
print()
|
||||
info("Initramfs should be rebuilt to include NVIDIA modules.")
|
||||
response = prompt("Rebuild initramfs now? (y/n): ").strip().lower()
|
||||
|
||||
if response == "y":
|
||||
rebuild_initramfs()
|
||||
print()
|
||||
verify_initramfs()
|
||||
|
||||
print()
|
||||
success("=== NVIDIA Setup Complete ===")
|
||||
print()
|
||||
info("Next steps:")
|
||||
print(" 1. Install and configure GRUB bootloader")
|
||||
print(" 2. Verify initramfs has NVIDIA and crypt modules")
|
||||
print(" 3. Reboot and test")
|
||||
105
install/portage.py
Normal file
105
install/portage.py
Normal file
@ -0,0 +1,105 @@
|
||||
"""Portage configuration management."""
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from .utils import info, success, error
|
||||
|
||||
|
||||
def copy_portage_config(
|
||||
source_dir: Path,
|
||||
mount_root: Path | None = None,
|
||||
) -> None:
|
||||
"""Copy Portage configuration files to the new system."""
|
||||
if mount_root is None:
|
||||
mount_root = Path("/mnt/gentoo")
|
||||
|
||||
info("=== Copying Portage Configuration ===")
|
||||
|
||||
portage_dst = mount_root / "etc/portage"
|
||||
etc_dst = mount_root / "etc"
|
||||
|
||||
# Source directories
|
||||
portage_src = source_dir / "portage"
|
||||
|
||||
# Check if already configured (idempotency check)
|
||||
make_conf = portage_dst / "make.conf"
|
||||
if make_conf.exists():
|
||||
info("Portage configuration already exists, updating...")
|
||||
|
||||
# Files to copy directly to /etc/portage/
|
||||
portage_files = ["make.conf"]
|
||||
|
||||
# Directories to copy to /etc/portage/
|
||||
portage_dirs = [
|
||||
"package.accept_keywords",
|
||||
"package.use",
|
||||
"package.mask",
|
||||
"package.env",
|
||||
"package.license",
|
||||
"env",
|
||||
"sets",
|
||||
]
|
||||
|
||||
# Directories to copy to /etc/ (from source_dir root, not portage/)
|
||||
etc_dirs = ["dracut.conf.d"]
|
||||
|
||||
# Copy portage files (from portage/ subdir)
|
||||
for filename in portage_files:
|
||||
src = portage_src / filename
|
||||
if src.exists():
|
||||
dst = portage_dst / filename
|
||||
shutil.copy2(src, dst)
|
||||
success(f"Copied {filename}")
|
||||
else:
|
||||
error(f"Warning: {filename} not found in {portage_src}")
|
||||
|
||||
# Copy portage directories (from portage/ subdir, merge don't destroy)
|
||||
for dirname in portage_dirs:
|
||||
src = portage_src / dirname
|
||||
if src.is_dir():
|
||||
dst = portage_dst / dirname
|
||||
shutil.copytree(src, dst, dirs_exist_ok=True)
|
||||
success(f"Copied {dirname}/")
|
||||
|
||||
# Copy etc directories (from source_dir root, merge don't destroy)
|
||||
for dirname in etc_dirs:
|
||||
src = source_dir / dirname
|
||||
if src.is_dir():
|
||||
dst = etc_dst / dirname
|
||||
shutil.copytree(src, dst, dirs_exist_ok=True)
|
||||
success(f"Copied {dirname}/")
|
||||
|
||||
print()
|
||||
success("=== Portage configuration copied ===")
|
||||
|
||||
|
||||
def copy_user_config(
|
||||
source_dir: Path,
|
||||
mount_root: Path | None = None,
|
||||
username: str = "damien",
|
||||
) -> None:
|
||||
"""Copy user configuration files."""
|
||||
if mount_root is None:
|
||||
mount_root = Path("/mnt/gentoo")
|
||||
|
||||
info("=== Copying User Configuration ===")
|
||||
|
||||
home_dir = mount_root / "home" / username
|
||||
|
||||
# Files to copy to home directory
|
||||
user_files = [".zshrc", "starship.toml"]
|
||||
|
||||
for filename in user_files:
|
||||
src = source_dir / filename
|
||||
if src.exists():
|
||||
if filename == "starship.toml":
|
||||
dst = home_dir / ".config" / filename
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
dst = home_dir / filename
|
||||
shutil.copy2(src, dst)
|
||||
success(f"Copied {filename}")
|
||||
|
||||
print()
|
||||
success("=== User configuration copied ===")
|
||||
213
install/services.py
Normal file
213
install/services.py
Normal file
@ -0,0 +1,213 @@
|
||||
"""Service packages installation and OpenRC configuration."""
|
||||
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from .utils import info, success, error, run, emerge
|
||||
|
||||
|
||||
# Packages to emerge for system services
|
||||
SERVICE_PACKAGES = [
|
||||
# System essentials
|
||||
"app-admin/sysklogd",
|
||||
"sys-process/cronie",
|
||||
# Network
|
||||
"net-misc/networkmanager",
|
||||
"net-misc/chrony",
|
||||
"net-firewall/iptables",
|
||||
# Bluetooth
|
||||
"net-wireless/bluez",
|
||||
"net-wireless/blueman",
|
||||
# Desktop services
|
||||
"sys-fs/udisks",
|
||||
"sys-power/power-profiles-daemon",
|
||||
"sys-auth/rtkit",
|
||||
# Display manager
|
||||
"x11-misc/sddm",
|
||||
"gui-libs/display-manager-init",
|
||||
# Containers
|
||||
"app-containers/podman",
|
||||
"app-containers/podman-compose",
|
||||
"app-containers/podman-tui",
|
||||
# Backup
|
||||
"app-backup/snapper",
|
||||
]
|
||||
|
||||
# OpenRC boot runlevel services
|
||||
BOOT_SERVICES = [
|
||||
"dbus",
|
||||
"elogind",
|
||||
"dmcrypt",
|
||||
]
|
||||
|
||||
# OpenRC default runlevel services
|
||||
DEFAULT_SERVICES = [
|
||||
"sysklogd",
|
||||
"sshd",
|
||||
"NetworkManager",
|
||||
"chronyd",
|
||||
"cronie",
|
||||
"power-profiles-daemon",
|
||||
"display-manager",
|
||||
"bluetooth",
|
||||
"iptables",
|
||||
"ip6tables",
|
||||
]
|
||||
|
||||
|
||||
def emerge_services() -> None:
|
||||
"""Emerge service packages."""
|
||||
info("=== Emerging Service Packages ===")
|
||||
|
||||
print()
|
||||
info("Packages to install:")
|
||||
for pkg in SERVICE_PACKAGES:
|
||||
print(f" - {pkg}")
|
||||
print()
|
||||
|
||||
emerge(*SERVICE_PACKAGES)
|
||||
|
||||
success("Service packages installed.")
|
||||
|
||||
|
||||
def configure_display_manager() -> None:
|
||||
"""Configure SDDM as the display manager."""
|
||||
info("=== Configuring Display Manager ===")
|
||||
|
||||
dm_conf = Path("/etc/conf.d/display-manager")
|
||||
|
||||
if dm_conf.exists():
|
||||
content = dm_conf.read_text()
|
||||
|
||||
# Check if already configured correctly (idempotency)
|
||||
if 'DISPLAYMANAGER="sddm"' in content:
|
||||
info("SDDM already configured as display manager")
|
||||
return
|
||||
|
||||
# Update existing config
|
||||
lines = content.split("\n")
|
||||
new_lines = []
|
||||
found = False
|
||||
|
||||
for line in lines:
|
||||
if line.startswith("DISPLAYMANAGER="):
|
||||
new_lines.append('DISPLAYMANAGER="sddm"')
|
||||
found = True
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
if not found:
|
||||
new_lines.append('DISPLAYMANAGER="sddm"')
|
||||
|
||||
dm_conf.write_text("\n".join(new_lines))
|
||||
else:
|
||||
dm_conf.write_text('DISPLAYMANAGER="sddm"\n')
|
||||
|
||||
success("SDDM configured as display manager.")
|
||||
|
||||
|
||||
def copy_system_configs(source_dir: Path) -> None:
|
||||
"""Copy system configuration files."""
|
||||
info("=== Copying System Configuration ===")
|
||||
|
||||
# conf.d/ -> /etc/conf.d/
|
||||
conf_d_src = source_dir / "conf.d"
|
||||
if conf_d_src.is_dir():
|
||||
conf_d_dst = Path("/etc/conf.d")
|
||||
for f in conf_d_src.iterdir():
|
||||
if f.is_file():
|
||||
shutil.copy2(f, conf_d_dst / f.name)
|
||||
success(f"Copied conf.d/{f.name}")
|
||||
|
||||
# iptables/ -> /etc/iptables/ (for reference)
|
||||
# Also copy to /var/lib/iptables/ and /var/lib/ip6tables/ (OpenRC locations)
|
||||
iptables_src = source_dir / "iptables"
|
||||
if iptables_src.is_dir():
|
||||
# Copy to /etc/iptables for reference
|
||||
iptables_etc = Path("/etc/iptables")
|
||||
iptables_etc.mkdir(parents=True, exist_ok=True)
|
||||
for f in iptables_src.iterdir():
|
||||
if f.is_file():
|
||||
shutil.copy2(f, iptables_etc / f.name)
|
||||
success(f"Copied iptables/{f.name}")
|
||||
|
||||
# Copy to OpenRC service locations
|
||||
iptables_var = Path("/var/lib/iptables")
|
||||
ip6tables_var = Path("/var/lib/ip6tables")
|
||||
iptables_var.mkdir(parents=True, exist_ok=True)
|
||||
ip6tables_var.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
rules_v4 = iptables_src / "iptables.rules"
|
||||
rules_v6 = iptables_src / "ip6tables.rules"
|
||||
|
||||
if rules_v4.exists():
|
||||
shutil.copy2(rules_v4, iptables_var / "rules-save")
|
||||
success("Installed iptables rules to /var/lib/iptables/rules-save")
|
||||
|
||||
if rules_v6.exists():
|
||||
shutil.copy2(rules_v6, ip6tables_var / "rules-save")
|
||||
success("Installed ip6tables rules to /var/lib/ip6tables/rules-save")
|
||||
|
||||
# udev/ -> /etc/udev/rules.d/
|
||||
udev_src = source_dir / "udev"
|
||||
if udev_src.is_dir():
|
||||
udev_dst = Path("/etc/udev/rules.d")
|
||||
udev_dst.mkdir(parents=True, exist_ok=True)
|
||||
for f in udev_src.iterdir():
|
||||
if f.is_file():
|
||||
shutil.copy2(f, udev_dst / f.name)
|
||||
success(f"Copied udev/{f.name}")
|
||||
|
||||
|
||||
def setup_openrc_services() -> None:
|
||||
"""Add services to OpenRC runlevels."""
|
||||
info("=== Configuring OpenRC Services ===")
|
||||
|
||||
# Boot runlevel
|
||||
info("Adding boot runlevel services...")
|
||||
for service in BOOT_SERVICES:
|
||||
result = run("rc-update", "add", service, "boot", check=False)
|
||||
if result.ok:
|
||||
success(f" Added {service} to boot")
|
||||
else:
|
||||
error(f" Warning: Could not add {service} (may already exist)")
|
||||
|
||||
# Default runlevel
|
||||
info("Adding default runlevel services...")
|
||||
for service in DEFAULT_SERVICES:
|
||||
result = run("rc-update", "add", service, "default", check=False)
|
||||
if result.ok:
|
||||
success(f" Added {service} to default")
|
||||
else:
|
||||
error(f" Warning: Could not add {service} (may already exist)")
|
||||
|
||||
print()
|
||||
info("Current runlevel configuration:")
|
||||
run("rc-update", "show")
|
||||
|
||||
|
||||
def setup_services(source_dir: Path | None = None) -> None:
|
||||
"""Full services setup workflow."""
|
||||
if source_dir is None:
|
||||
# Assume config files are in /root/gentoo or similar
|
||||
# This should be passed from setup.py
|
||||
source_dir = Path("/root/gentoo")
|
||||
|
||||
emerge_services()
|
||||
print()
|
||||
configure_display_manager()
|
||||
print()
|
||||
copy_system_configs(source_dir)
|
||||
print()
|
||||
setup_openrc_services()
|
||||
|
||||
print()
|
||||
success("=== Services Setup Complete ===")
|
||||
print()
|
||||
info("Next steps:")
|
||||
print(" 1. Install kernel: emerge gentoo-kernel-bin (or gentoo-kernel)")
|
||||
print(" 2. Configure bootloader")
|
||||
print(" 3. Set root password: passwd")
|
||||
print(" 4. Create user account")
|
||||
print(" 5. Reboot and test")
|
||||
179
install/stage3.py
Normal file
179
install/stage3.py
Normal file
@ -0,0 +1,179 @@
|
||||
"""Stage3 tarball download and extraction."""
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from urllib.request import urlopen, Request
|
||||
from urllib.error import URLError
|
||||
|
||||
from .utils import info, success, error, fatal, run
|
||||
|
||||
|
||||
@dataclass
|
||||
class Stage3Config:
|
||||
"""Configuration for stage3 download."""
|
||||
init_system: str = "openrc" # or "systemd"
|
||||
mirrors: list[str] = field(default_factory=lambda: [
|
||||
"https://mirrors.rit.edu/gentoo/",
|
||||
"https://distfiles.gentoo.org/",
|
||||
"https://gentoo.osuosl.org/",
|
||||
])
|
||||
mount_root: Path = field(default_factory=lambda: Path("/mnt/gentoo"))
|
||||
|
||||
@property
|
||||
def variant(self) -> str:
|
||||
return f"stage3-amd64-{self.init_system}"
|
||||
|
||||
|
||||
def _fetch_url(url: str, timeout: int = 30) -> bytes:
|
||||
"""Fetch URL content."""
|
||||
request = Request(url, headers={"User-Agent": "install-installer/4.0"})
|
||||
with urlopen(request, timeout=timeout) as response:
|
||||
return response.read()
|
||||
|
||||
|
||||
def _find_stage3_filename(mirror: str, config: Stage3Config) -> str | None:
|
||||
"""Find current stage3 filename from mirror."""
|
||||
base_url = f"{mirror}releases/amd64/autobuilds/current-{config.variant}/"
|
||||
latest_url = f"{base_url}latest-{config.variant}.txt"
|
||||
|
||||
try:
|
||||
content = _fetch_url(latest_url).decode("utf-8")
|
||||
except URLError as e:
|
||||
error(f"Failed to fetch {latest_url}: {e}")
|
||||
return None
|
||||
|
||||
# Parse PGP-signed content or direct listing
|
||||
pattern = rf"{config.variant}-\d{{8}}T\d{{6}}Z\.tar\.xz"
|
||||
match = re.search(pattern, content)
|
||||
|
||||
if match:
|
||||
return match.group(0)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _verify_sha512(filepath: Path, expected: str) -> bool:
|
||||
"""Verify SHA512 checksum of file."""
|
||||
sha512 = hashlib.sha512()
|
||||
with open(filepath, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(8192), b""):
|
||||
sha512.update(chunk)
|
||||
return sha512.hexdigest() == expected
|
||||
|
||||
|
||||
def _parse_digests(content: str, filename: str) -> str | None:
|
||||
"""Extract SHA512 hash from DIGESTS file."""
|
||||
lines = content.split("\n")
|
||||
for i, line in enumerate(lines):
|
||||
if "SHA512" in line and i + 1 < len(lines) and filename in lines[i + 1]:
|
||||
# Hash is on next line, first field
|
||||
hash_line = lines[i + 1].strip()
|
||||
return hash_line.split()[0]
|
||||
|
||||
# Alternative format: hash followed by filename on same line
|
||||
for line in lines:
|
||||
if filename in line and len(line.split()) >= 2:
|
||||
parts = line.split()
|
||||
if len(parts[0]) == 128: # SHA512 is 128 hex chars
|
||||
return parts[0]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def download_stage3(config: Stage3Config | None = None) -> Path:
|
||||
"""Download and verify stage3 tarball."""
|
||||
if config is None:
|
||||
config = Stage3Config()
|
||||
|
||||
info(f"=== Downloading Stage3 ({config.init_system}) ===")
|
||||
|
||||
filename = None
|
||||
working_mirror = None
|
||||
|
||||
for mirror in config.mirrors:
|
||||
info(f"Trying mirror: {mirror}")
|
||||
filename = _find_stage3_filename(mirror, config)
|
||||
if filename:
|
||||
working_mirror = mirror
|
||||
success(f"Found: {filename}")
|
||||
break
|
||||
error(f"Mirror {mirror} failed, trying next...")
|
||||
|
||||
if not filename or not working_mirror:
|
||||
fatal("Could not find stage3 on any mirror")
|
||||
|
||||
base_url = f"{working_mirror}releases/amd64/autobuilds/current-{config.variant}/"
|
||||
tarball_url = f"{base_url}{filename}"
|
||||
digests_url = f"{base_url}{filename}.DIGESTS"
|
||||
|
||||
target_dir = config.mount_root
|
||||
tarball_path = target_dir / filename
|
||||
digests_path = target_dir / f"{filename}.DIGESTS"
|
||||
|
||||
# Download tarball
|
||||
info(f"Downloading {filename}...")
|
||||
run("wget", "--progress=bar:force", "-O", str(tarball_path), tarball_url)
|
||||
|
||||
# Download digests
|
||||
info("Downloading DIGESTS...")
|
||||
run("wget", "-q", "-O", str(digests_path), digests_url)
|
||||
|
||||
# Verify checksum
|
||||
info("Verifying SHA512 checksum...")
|
||||
digests_content = digests_path.read_text()
|
||||
expected_hash = _parse_digests(digests_content, filename)
|
||||
|
||||
if not expected_hash:
|
||||
error("Could not parse checksum from DIGESTS file")
|
||||
error("Continuing without verification (manual check recommended)")
|
||||
else:
|
||||
if _verify_sha512(tarball_path, expected_hash):
|
||||
success("Checksum verified.")
|
||||
else:
|
||||
fatal("Checksum verification FAILED")
|
||||
|
||||
# Cleanup digests file
|
||||
digests_path.unlink()
|
||||
|
||||
return tarball_path
|
||||
|
||||
|
||||
def extract_stage3(tarball_path: Path, mount_root: Path | None = None) -> None:
|
||||
"""Extract stage3 tarball."""
|
||||
if mount_root is None:
|
||||
mount_root = Path("/mnt/gentoo")
|
||||
|
||||
info(f"Extracting {tarball_path.name}...")
|
||||
|
||||
run(
|
||||
"tar", "xpf", str(tarball_path),
|
||||
"--xattrs-include=*.*",
|
||||
"--numeric-owner",
|
||||
"--skip-old-files", # Resume capability - skip files that already exist
|
||||
"-C", str(mount_root),
|
||||
)
|
||||
|
||||
success("Stage3 extracted.")
|
||||
|
||||
# Cleanup tarball
|
||||
tarball_path.unlink()
|
||||
success("Cleaned up tarball.")
|
||||
|
||||
|
||||
def fetch_stage3(config: Stage3Config | None = None) -> None:
|
||||
"""Full stage3 workflow: download, verify, extract."""
|
||||
if config is None:
|
||||
config = Stage3Config()
|
||||
|
||||
# Check if already extracted (idempotency check)
|
||||
if (config.mount_root / "etc/portage").exists():
|
||||
info("Stage3 already extracted (skipping)")
|
||||
return
|
||||
|
||||
tarball = download_stage3(config)
|
||||
extract_stage3(tarball, config.mount_root)
|
||||
|
||||
print()
|
||||
success("=== Stage3 installation complete ===")
|
||||
324
install/sync.py
Normal file
324
install/sync.py
Normal file
@ -0,0 +1,324 @@
|
||||
"""Initial portage sync and profile setup (run inside chroot)."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from .utils import info, success, error, fatal, prompt, run, run_quiet, emerge
|
||||
|
||||
|
||||
DESIRED_PROFILE = "default/linux/amd64/23.0/desktop"
|
||||
SWAP_MAPPER = "swap"
|
||||
|
||||
# System defaults
|
||||
DEFAULT_TIMEZONE = "America/New_York"
|
||||
DEFAULT_LOCALE = "en_US.UTF-8"
|
||||
DEFAULT_HOSTNAME = "legion"
|
||||
|
||||
|
||||
def get_swap_partition() -> Path | None:
|
||||
"""Read swap partition from /etc/conf.d/dmcrypt."""
|
||||
dmcrypt_conf = Path("/etc/conf.d/dmcrypt")
|
||||
if not dmcrypt_conf.exists():
|
||||
return None
|
||||
|
||||
content = dmcrypt_conf.read_text()
|
||||
for line in content.split("\n"):
|
||||
if line.startswith("source="):
|
||||
# Extract path from source='...' or source="..."
|
||||
match = re.match(r"source=['\"]?([^'\"]+)['\"]?", line)
|
||||
if match:
|
||||
return Path(match.group(1))
|
||||
return None
|
||||
|
||||
|
||||
def activate_swap() -> None:
|
||||
"""Activate encrypted swap for use during compilation.
|
||||
|
||||
In a chroot, OpenRC isn't running, so we manually set up swap.
|
||||
"""
|
||||
info("=== Activating Encrypted Swap ===")
|
||||
|
||||
swap_part = get_swap_partition()
|
||||
if swap_part is None:
|
||||
error("Could not find swap partition in /etc/conf.d/dmcrypt")
|
||||
error("Skipping swap activation - builds may be slower")
|
||||
return
|
||||
|
||||
mapper_path = Path(f"/dev/mapper/{SWAP_MAPPER}")
|
||||
|
||||
# Check if already active
|
||||
if mapper_path.exists():
|
||||
info("Swap already active")
|
||||
run("swapon", "--show")
|
||||
return
|
||||
|
||||
info(f"Setting up encrypted swap on {swap_part}...")
|
||||
|
||||
# Create encrypted swap with random key
|
||||
run(
|
||||
"cryptsetup", "open",
|
||||
"--type", "plain",
|
||||
"--cipher", "aes-xts-plain64",
|
||||
"--key-size", "256",
|
||||
"--key-file", "/dev/urandom",
|
||||
str(swap_part), SWAP_MAPPER,
|
||||
)
|
||||
|
||||
# Format as swap
|
||||
run("mkswap", str(mapper_path))
|
||||
|
||||
# Activate
|
||||
run("swapon", str(mapper_path))
|
||||
|
||||
success("Encrypted swap activated.")
|
||||
run("swapon", "--show")
|
||||
|
||||
|
||||
def deactivate_swap() -> None:
|
||||
"""Deactivate encrypted swap before exiting chroot."""
|
||||
mapper_path = Path(f"/dev/mapper/{SWAP_MAPPER}")
|
||||
|
||||
if not mapper_path.exists():
|
||||
return
|
||||
|
||||
info("Deactivating encrypted swap...")
|
||||
run("swapoff", str(mapper_path), check=False)
|
||||
run("cryptsetup", "close", SWAP_MAPPER, check=False)
|
||||
success("Swap deactivated.")
|
||||
|
||||
|
||||
def emerge_webrsync() -> None:
|
||||
"""Initial portage tree sync via webrsync."""
|
||||
info("=== Syncing Portage Tree (webrsync) ===")
|
||||
run("emerge-webrsync")
|
||||
success("Portage tree synced.")
|
||||
|
||||
|
||||
def list_profiles() -> list[tuple[int, str, bool]]:
|
||||
"""List available profiles and return parsed list.
|
||||
|
||||
Returns list of (number, profile_name, is_selected).
|
||||
"""
|
||||
result = run_quiet("eselect", "profile", "list")
|
||||
|
||||
profiles = []
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
# Format: " [N] profile/name (stable) *"
|
||||
# The * indicates currently selected
|
||||
match = re.match(r'\s*\[(\d+)\]\s+(\S+)(?:\s+\(.*\))?(\s+\*)?', line)
|
||||
if match:
|
||||
num = int(match.group(1))
|
||||
name = match.group(2)
|
||||
selected = match.group(3) is not None
|
||||
profiles.append((num, name, selected))
|
||||
|
||||
return profiles
|
||||
|
||||
|
||||
def find_profile_number(profiles: list[tuple[int, str, bool]], target: str) -> int | None:
|
||||
"""Find profile number for a given profile name."""
|
||||
for num, name, _ in profiles:
|
||||
if name == target:
|
||||
return num
|
||||
return None
|
||||
|
||||
|
||||
def select_profile() -> None:
|
||||
"""Interactive profile selection."""
|
||||
info("=== Profile Selection ===")
|
||||
|
||||
profiles = list_profiles()
|
||||
|
||||
# Display profiles
|
||||
print()
|
||||
info("Available profiles:")
|
||||
run("eselect", "profile", "list")
|
||||
|
||||
# Find desired profile
|
||||
target_num = find_profile_number(profiles, DESIRED_PROFILE)
|
||||
|
||||
if target_num is None:
|
||||
error(f"Could not find profile: {DESIRED_PROFILE}")
|
||||
print()
|
||||
choice = prompt("Enter profile number manually: ")
|
||||
try:
|
||||
target_num = int(choice)
|
||||
except ValueError:
|
||||
fatal("Invalid profile number")
|
||||
else:
|
||||
print()
|
||||
success(f"Recommended: [{target_num}] {DESIRED_PROFILE}")
|
||||
|
||||
response = prompt(f"Set profile to {DESIRED_PROFILE}? (y/n/other number): ").strip()
|
||||
|
||||
if response.lower() == "n":
|
||||
fatal("Profile selection cancelled")
|
||||
elif response.lower() != "y":
|
||||
try:
|
||||
target_num = int(response)
|
||||
except ValueError:
|
||||
fatal("Invalid input")
|
||||
|
||||
# Set the profile
|
||||
info(f"Setting profile to [{target_num}]...")
|
||||
run("eselect", "profile", "set", str(target_num))
|
||||
|
||||
# Verify
|
||||
print()
|
||||
info("Current profile:")
|
||||
run("eselect", "profile", "show")
|
||||
success("Profile set.")
|
||||
|
||||
|
||||
def setup_repositories() -> None:
|
||||
"""Install eselect-repository and enable guru overlay."""
|
||||
info("=== Setting Up Repositories ===")
|
||||
|
||||
# Install eselect-repository
|
||||
info("Installing eselect-repository...")
|
||||
emerge("app-eselect/eselect-repository", ask=False, verbose=False)
|
||||
success("eselect-repository installed.")
|
||||
|
||||
# Enable guru overlay
|
||||
info("Enabling guru overlay...")
|
||||
run("eselect", "repository", "enable", "guru")
|
||||
success("guru overlay enabled.")
|
||||
|
||||
# Sync guru
|
||||
info("Syncing guru overlay...")
|
||||
run("emerge", "--sync", "guru")
|
||||
success("guru overlay synced.")
|
||||
|
||||
|
||||
def setup_timezone(tz: str = DEFAULT_TIMEZONE) -> None:
|
||||
"""Set system timezone."""
|
||||
info(f"=== Setting Timezone: {tz} ===")
|
||||
|
||||
tz_file = Path(f"/usr/share/zoneinfo/{tz}")
|
||||
if not tz_file.exists():
|
||||
error(f"Timezone {tz} not found")
|
||||
return
|
||||
|
||||
localtime = Path("/etc/localtime")
|
||||
|
||||
# Check if already set correctly (idempotency)
|
||||
if localtime.is_symlink():
|
||||
try:
|
||||
if localtime.resolve() == tz_file.resolve():
|
||||
info(f"Timezone already set to {tz}")
|
||||
return
|
||||
except OSError:
|
||||
pass # Broken symlink, continue to fix it
|
||||
|
||||
if localtime.exists() or localtime.is_symlink():
|
||||
localtime.unlink()
|
||||
|
||||
localtime.symlink_to(tz_file)
|
||||
success(f"Timezone set to {tz}")
|
||||
|
||||
|
||||
def setup_locale(locale: str = DEFAULT_LOCALE) -> None:
|
||||
"""Configure locale.gen and generate locales."""
|
||||
info(f"=== Configuring Locale: {locale} ===")
|
||||
|
||||
locale_gen = Path("/etc/locale.gen")
|
||||
|
||||
# Read existing content
|
||||
if locale_gen.exists():
|
||||
content = locale_gen.read_text()
|
||||
else:
|
||||
content = ""
|
||||
|
||||
# Check if locale is already uncommented
|
||||
locale_line = f"{locale} UTF-8"
|
||||
if locale_line in content and not f"#{locale_line}" in content:
|
||||
info(f"{locale} already enabled")
|
||||
else:
|
||||
# Uncomment or append the locale
|
||||
if f"#{locale_line}" in content:
|
||||
content = content.replace(f"#{locale_line}", locale_line)
|
||||
elif f"# {locale_line}" in content:
|
||||
content = content.replace(f"# {locale_line}", locale_line)
|
||||
else:
|
||||
content += f"\n{locale_line}\n"
|
||||
|
||||
locale_gen.write_text(content)
|
||||
success(f"Enabled {locale} in /etc/locale.gen")
|
||||
|
||||
# Generate locales
|
||||
info("Running locale-gen...")
|
||||
run("locale-gen")
|
||||
|
||||
# Set system locale
|
||||
env_locale = Path("/etc/env.d/02locale")
|
||||
env_locale.write_text(f'LANG="{locale}"\nLC_COLLATE="C.UTF-8"\n')
|
||||
success(f"Set LANG={locale} in /etc/env.d/02locale")
|
||||
|
||||
# Update environment
|
||||
run("env-update", check=False)
|
||||
|
||||
|
||||
def setup_hostname(hostname: str = DEFAULT_HOSTNAME) -> None:
|
||||
"""Set system hostname."""
|
||||
info(f"=== Setting Hostname: {hostname} ===")
|
||||
|
||||
# /etc/hostname
|
||||
hostname_file = Path("/etc/hostname")
|
||||
|
||||
# Check if already set correctly (idempotency)
|
||||
if hostname_file.exists() and hostname_file.read_text().strip() == hostname:
|
||||
info(f"Hostname already set to {hostname}")
|
||||
return
|
||||
|
||||
hostname_file.write_text(f"{hostname}\n")
|
||||
success(f"Set /etc/hostname to {hostname}")
|
||||
|
||||
# /etc/conf.d/hostname (OpenRC)
|
||||
conf_hostname = Path("/etc/conf.d/hostname")
|
||||
conf_hostname.write_text(f'hostname="{hostname}"\n')
|
||||
success("Set /etc/conf.d/hostname")
|
||||
|
||||
# Update /etc/hosts
|
||||
hosts_file = Path("/etc/hosts")
|
||||
if hosts_file.exists():
|
||||
content = hosts_file.read_text()
|
||||
# Check if we need to add the hostname
|
||||
if hostname not in content:
|
||||
# Add hostname to localhost line
|
||||
lines = content.split("\n")
|
||||
new_lines = []
|
||||
for line in lines:
|
||||
if line.startswith("127.0.0.1"):
|
||||
if hostname not in line:
|
||||
line = f"{line} {hostname}"
|
||||
new_lines.append(line)
|
||||
hosts_file.write_text("\n".join(new_lines))
|
||||
success(f"Added {hostname} to /etc/hosts")
|
||||
|
||||
|
||||
def initial_sync() -> None:
|
||||
"""Full initial sync workflow."""
|
||||
activate_swap()
|
||||
print()
|
||||
emerge_webrsync()
|
||||
print()
|
||||
select_profile()
|
||||
print()
|
||||
setup_repositories()
|
||||
print()
|
||||
setup_timezone()
|
||||
print()
|
||||
setup_locale()
|
||||
print()
|
||||
setup_hostname()
|
||||
|
||||
print()
|
||||
success("=== Initial Portage Setup Complete ===")
|
||||
print()
|
||||
info("Swap is active for compilation. Next steps:")
|
||||
print(" 1. Review /etc/portage/make.conf")
|
||||
print(" 2. emerge -avuDN @world")
|
||||
print(" 3. Install kernel and bootloader")
|
||||
print()
|
||||
info("When done, deactivate swap before exiting chroot:")
|
||||
print(" python setup.py swap-off")
|
||||
195
install/users.py
Normal file
195
install/users.py
Normal file
@ -0,0 +1,195 @@
|
||||
"""User configuration and shell setup."""
|
||||
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from .utils import info, success, error, run, run_quiet, emerge, prompt
|
||||
|
||||
|
||||
# Shell and portage tool packages
|
||||
SHELL_PACKAGES = [
|
||||
# Shell
|
||||
"app-shells/zsh",
|
||||
"app-shells/gentoo-zsh-completions",
|
||||
"app-shells/zsh-autosuggestions",
|
||||
"app-shells/zsh-syntax-highlighting",
|
||||
# Prompt & tools
|
||||
"app-shells/starship",
|
||||
"app-shells/zoxide",
|
||||
"app-shells/fzf",
|
||||
"sys-apps/lsd",
|
||||
# Portage tools
|
||||
"app-portage/gentoolkit",
|
||||
"app-portage/portage-utils",
|
||||
"app-portage/eix",
|
||||
]
|
||||
|
||||
# User groups for desktop use
|
||||
# Minimal set - elogind grants device access to active session
|
||||
USER_GROUPS = [
|
||||
"users",
|
||||
"wheel", # sudo access
|
||||
"input", # Wayland compositor input devices
|
||||
]
|
||||
|
||||
|
||||
def emerge_shell_packages() -> None:
|
||||
"""Emerge shell and portage tool packages."""
|
||||
info("=== Installing Shell & Portage Tools ===")
|
||||
|
||||
print()
|
||||
info("Packages to install:")
|
||||
for pkg in SHELL_PACKAGES:
|
||||
print(f" - {pkg}")
|
||||
print()
|
||||
|
||||
emerge(*SHELL_PACKAGES)
|
||||
|
||||
success("Shell packages installed.")
|
||||
|
||||
|
||||
def install_sudo() -> None:
|
||||
"""Install and configure sudo."""
|
||||
info("=== Installing sudo ===")
|
||||
|
||||
emerge("app-admin/sudo")
|
||||
|
||||
# Configure sudoers - enable wheel group
|
||||
sudoers = Path("/etc/sudoers")
|
||||
if sudoers.exists():
|
||||
content = sudoers.read_text()
|
||||
|
||||
# Check if wheel group already has sudo access (any format)
|
||||
if re.search(r"^%wheel\s+ALL=", content, re.MULTILINE):
|
||||
success("Wheel group already enabled in sudoers")
|
||||
elif re.search(r"^#\s*%wheel\s+ALL=", content, re.MULTILINE):
|
||||
# Uncomment the existing commented line
|
||||
content = re.sub(
|
||||
r"^#\s*(%wheel\s+ALL=.*$)",
|
||||
r"\1",
|
||||
content,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
sudoers.write_text(content)
|
||||
success("Enabled wheel group in sudoers")
|
||||
else:
|
||||
# Append if not present at all
|
||||
with open(sudoers, "a") as f:
|
||||
f.write("\n# Allow wheel group to use sudo\n")
|
||||
f.write("%wheel ALL=(ALL:ALL) ALL\n")
|
||||
success("Added wheel group to sudoers")
|
||||
|
||||
success("sudo configured.")
|
||||
|
||||
|
||||
def copy_shell_configs(source_dir: Path, target_home: Path, owner: str | None = None) -> None:
|
||||
"""Copy shell configuration files to a home directory."""
|
||||
# .zshrc
|
||||
zshrc_src = source_dir / ".zshrc"
|
||||
if zshrc_src.exists():
|
||||
shutil.copy2(zshrc_src, target_home / ".zshrc")
|
||||
success(f"Copied .zshrc to {target_home}")
|
||||
|
||||
# starship.toml
|
||||
starship_src = source_dir / "starship.toml"
|
||||
if starship_src.exists():
|
||||
config_dir = target_home / ".config"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(starship_src, config_dir / "starship.toml")
|
||||
success(f"Copied starship.toml to {config_dir}")
|
||||
|
||||
# Fix ownership if specified
|
||||
if owner:
|
||||
run("chown", "-R", f"{owner}:{owner}", str(target_home), check=False)
|
||||
|
||||
|
||||
def setup_root_shell(source_dir: Path) -> None:
|
||||
"""Configure root's shell environment."""
|
||||
info("=== Configuring Root Shell ===")
|
||||
|
||||
copy_shell_configs(source_dir, Path("/root"))
|
||||
|
||||
# Change root's shell to zsh
|
||||
run("chsh", "-s", "/bin/zsh", "root")
|
||||
success("Root shell set to zsh")
|
||||
|
||||
|
||||
def set_root_password() -> None:
|
||||
"""Prompt to set root password."""
|
||||
info("=== Set Root Password ===")
|
||||
print()
|
||||
run("passwd")
|
||||
print()
|
||||
|
||||
|
||||
def create_user(username: str, source_dir: Path) -> None:
|
||||
"""Create a new user with proper groups and shell config."""
|
||||
info(f"=== Creating User: {username} ===")
|
||||
|
||||
# Build groups string
|
||||
groups = ",".join(USER_GROUPS)
|
||||
|
||||
# Check if user already exists using id command (more reliable than string matching)
|
||||
result = run_quiet("id", username, check=False)
|
||||
if result.ok:
|
||||
info(f"User {username} already exists, ensuring groups are correct...")
|
||||
run("usermod", "-aG", groups, username, check=False)
|
||||
else:
|
||||
# Create user
|
||||
result = run(
|
||||
"useradd", "-m",
|
||||
"-G", groups,
|
||||
"-s", "/bin/zsh",
|
||||
username,
|
||||
check=False
|
||||
)
|
||||
if not result.ok:
|
||||
error(f"Failed to create user: {result.stderr}")
|
||||
return
|
||||
|
||||
success(f"User {username} configured with groups: {groups}")
|
||||
|
||||
# Copy shell configs
|
||||
user_home = Path(f"/home/{username}")
|
||||
copy_shell_configs(source_dir, user_home, owner=username)
|
||||
|
||||
# Set user password
|
||||
print()
|
||||
info(f"Set password for {username}:")
|
||||
run("passwd", username)
|
||||
print()
|
||||
|
||||
success(f"User {username} configured.")
|
||||
|
||||
|
||||
def setup_users(source_dir: Path | None = None) -> None:
|
||||
"""Full user setup workflow."""
|
||||
if source_dir is None:
|
||||
source_dir = Path("/root/gentoo")
|
||||
|
||||
emerge_shell_packages()
|
||||
print()
|
||||
install_sudo()
|
||||
print()
|
||||
setup_root_shell(source_dir)
|
||||
print()
|
||||
set_root_password()
|
||||
|
||||
# Prompt for username
|
||||
print()
|
||||
info("=== Create User Account ===")
|
||||
username = prompt("Enter username to create: ").strip()
|
||||
|
||||
if username:
|
||||
create_user(username, source_dir)
|
||||
else:
|
||||
info("Skipped user creation.")
|
||||
|
||||
print()
|
||||
success("=== User Setup Complete ===")
|
||||
print()
|
||||
info("Next steps:")
|
||||
print(" 1. Install @hyprland set for desktop environment")
|
||||
print(" 2. Configure display manager (SDDM)")
|
||||
print(" 3. Reboot and login as your user")
|
||||
209
install/utils.py
Normal file
209
install/utils.py
Normal file
@ -0,0 +1,209 @@
|
||||
"""Common utilities for Gentoo installation."""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import NoReturn
|
||||
|
||||
|
||||
# --- Colors ---
|
||||
|
||||
class Color:
|
||||
RED = "\033[0;31m"
|
||||
GREEN = "\033[0;32m"
|
||||
YELLOW = "\033[0;33m"
|
||||
BOLD = "\033[1m"
|
||||
NC = "\033[0m" # No color
|
||||
|
||||
|
||||
def info(msg: str) -> None:
|
||||
print(f"{Color.YELLOW}{msg}{Color.NC}")
|
||||
|
||||
|
||||
def success(msg: str) -> None:
|
||||
print(f"{Color.GREEN}{msg}{Color.NC}")
|
||||
|
||||
|
||||
def error(msg: str) -> None:
|
||||
print(f"{Color.RED}{msg}{Color.NC}", file=sys.stderr)
|
||||
|
||||
|
||||
def warn(msg: str) -> None:
|
||||
print(f"{Color.YELLOW}WARNING: {msg}{Color.NC}", file=sys.stderr)
|
||||
|
||||
|
||||
def fatal(msg: str) -> NoReturn:
|
||||
error(f"FATAL: {msg}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def prompt(msg: str) -> str:
|
||||
return input(f"{Color.YELLOW}{msg}{Color.NC}")
|
||||
|
||||
|
||||
def confirm(msg: str, require: str = "yes") -> bool:
|
||||
response = prompt(f"{msg} (type '{require}' to confirm): ")
|
||||
return response == require
|
||||
|
||||
|
||||
# --- Command Execution ---
|
||||
|
||||
@dataclass
|
||||
class RunResult:
|
||||
returncode: int
|
||||
stdout: str
|
||||
stderr: str
|
||||
|
||||
@property
|
||||
def ok(self) -> bool:
|
||||
return self.returncode == 0
|
||||
|
||||
|
||||
def run(
|
||||
*args: str,
|
||||
check: bool = True,
|
||||
capture: bool = False,
|
||||
quiet: bool = False,
|
||||
) -> RunResult:
|
||||
"""Run a command with proper error handling.
|
||||
|
||||
Args:
|
||||
*args: Command and arguments
|
||||
check: Raise exception on non-zero exit
|
||||
capture: Capture stdout/stderr (otherwise inherit terminal)
|
||||
quiet: Suppress command echo
|
||||
"""
|
||||
if not quiet:
|
||||
info(f"$ {' '.join(args)}")
|
||||
|
||||
result = subprocess.run(
|
||||
args,
|
||||
capture_output=capture,
|
||||
text=True,
|
||||
)
|
||||
|
||||
run_result = RunResult(
|
||||
returncode=result.returncode,
|
||||
stdout=result.stdout if capture else "",
|
||||
stderr=result.stderr if capture else "",
|
||||
)
|
||||
|
||||
if check and not run_result.ok:
|
||||
error(f"Command failed with exit code {result.returncode}")
|
||||
if capture and result.stderr:
|
||||
error(result.stderr)
|
||||
raise subprocess.CalledProcessError(result.returncode, args)
|
||||
|
||||
return run_result
|
||||
|
||||
|
||||
def run_quiet(*args: str, check: bool = True) -> RunResult:
|
||||
"""Run a command silently, capturing output."""
|
||||
return run(*args, check=check, capture=True, quiet=True)
|
||||
|
||||
|
||||
# --- System Checks ---
|
||||
|
||||
def check_root() -> None:
|
||||
"""Ensure running as root."""
|
||||
import os
|
||||
if os.geteuid() != 0:
|
||||
fatal("This script must be run as root")
|
||||
|
||||
|
||||
def check_uefi() -> None:
|
||||
"""Ensure booted in UEFI mode."""
|
||||
if not Path("/sys/firmware/efi/efivars").is_dir():
|
||||
fatal("UEFI boot mode not detected. This script requires UEFI.")
|
||||
success("UEFI boot mode confirmed.")
|
||||
|
||||
|
||||
def is_mounted(path: Path) -> bool:
|
||||
"""Check if a path is a mount point."""
|
||||
result = run_quiet("mountpoint", "-q", str(path), check=False)
|
||||
return result.ok
|
||||
|
||||
|
||||
def is_block_device(path: Path) -> bool:
|
||||
"""Check if path is a block device."""
|
||||
return path.is_block_device()
|
||||
|
||||
|
||||
# --- Portage Helpers ---
|
||||
|
||||
def dispatch_config() -> bool:
|
||||
"""Auto-merge pending portage config changes.
|
||||
|
||||
Returns True if changes were dispatched, False if none found.
|
||||
"""
|
||||
portage_dir = Path("/etc/portage")
|
||||
|
||||
# Check for pending changes
|
||||
has_cfg_files = bool(list(portage_dir.glob("._cfg*")))
|
||||
has_autounmask = any(
|
||||
(portage_dir / subdir / "zz-autounmask").exists()
|
||||
for subdir in ["package.use", "package.accept_keywords", "package.license"]
|
||||
)
|
||||
|
||||
if not has_cfg_files and not has_autounmask:
|
||||
return False
|
||||
|
||||
info("Auto-merging pending portage config changes...")
|
||||
|
||||
# Try dispatch-conf first, fall back to etc-update
|
||||
result = run(
|
||||
"bash", "-c", "yes u | dispatch-conf",
|
||||
check=False, capture=True, quiet=True
|
||||
)
|
||||
if not result.ok:
|
||||
run(
|
||||
"etc-update", "--automode", "-5",
|
||||
check=False, capture=True, quiet=True
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def emerge(
|
||||
*packages: str,
|
||||
ask: bool = True,
|
||||
verbose: bool = True,
|
||||
extra_args: list[str] | None = None,
|
||||
max_retries: int = 3,
|
||||
) -> None:
|
||||
"""Run emerge with automatic handling of USE/keyword/license changes.
|
||||
|
||||
Args:
|
||||
*packages: Package atoms to emerge
|
||||
ask: Prompt for confirmation (--ask)
|
||||
verbose: Show detailed output (--verbose)
|
||||
extra_args: Additional emerge arguments (e.g., ["--update", "--deep"])
|
||||
max_retries: Max attempts after auto-dispatching config changes
|
||||
"""
|
||||
args = ["emerge", "--autounmask-write", "--autounmask-continue"]
|
||||
if ask:
|
||||
args.append("--ask")
|
||||
if verbose:
|
||||
args.append("--verbose")
|
||||
if extra_args:
|
||||
args.extend(extra_args)
|
||||
args.extend(packages)
|
||||
|
||||
for attempt in range(1, max_retries + 1):
|
||||
if max_retries > 1:
|
||||
info(f"Emerge attempt {attempt}/{max_retries}...")
|
||||
|
||||
result = run(*args, check=False)
|
||||
|
||||
if result.ok:
|
||||
return
|
||||
|
||||
# Check if we can auto-dispatch and retry
|
||||
if dispatch_config():
|
||||
continue
|
||||
|
||||
# No config changes to dispatch, actual failure
|
||||
fatal(f"Emerge failed: {' '.join(packages)}")
|
||||
|
||||
fatal(f"Emerge failed after {max_retries} attempts")
|
||||
42
iptables/ip6tables.rules
Normal file
42
iptables/ip6tables.rules
Normal file
@ -0,0 +1,42 @@
|
||||
*filter
|
||||
:INPUT DROP [0:0]
|
||||
:FORWARD DROP [0:0]
|
||||
:OUTPUT ACCEPT [0:0]
|
||||
|
||||
# ============================================================
|
||||
# Stateful connection tracking
|
||||
# ============================================================
|
||||
|
||||
# Allow established and related connections
|
||||
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
|
||||
# ============================================================
|
||||
# Loopback interface - always allow
|
||||
# ============================================================
|
||||
|
||||
-A INPUT -i lo -j ACCEPT
|
||||
|
||||
# ============================================================
|
||||
# ICMPv6 - required for IPv6 neighbor discovery
|
||||
# ============================================================
|
||||
|
||||
-A INPUT -p icmpv6 --icmpv6-type router-solicitation -j ACCEPT
|
||||
-A INPUT -p icmpv6 --icmpv6-type router-advertisement -j ACCEPT
|
||||
-A INPUT -p icmpv6 --icmpv6-type neighbor-solicitation -j ACCEPT
|
||||
-A INPUT -p icmpv6 --icmpv6-type neighbor-advertisement -j ACCEPT
|
||||
-A INPUT -p icmpv6 --icmpv6-type echo-request -j ACCEPT
|
||||
-A INPUT -p icmpv6 --icmpv6-type echo-reply -j ACCEPT
|
||||
|
||||
# ============================================================
|
||||
# Link-local addresses only
|
||||
# ============================================================
|
||||
|
||||
-A INPUT -s fe80::/10 -j ACCEPT
|
||||
|
||||
# ============================================================
|
||||
# Default deny - drop everything not explicitly allowed
|
||||
# ============================================================
|
||||
|
||||
-A INPUT -j DROP
|
||||
|
||||
COMMIT
|
||||
42
iptables/iptables.rules
Normal file
42
iptables/iptables.rules
Normal file
@ -0,0 +1,42 @@
|
||||
*filter
|
||||
:INPUT DROP [0:0]
|
||||
:FORWARD DROP [0:0]
|
||||
:OUTPUT ACCEPT [0:0]
|
||||
|
||||
# ============================================================
|
||||
# Stateful connection tracking
|
||||
# ============================================================
|
||||
|
||||
# Allow established and related connections (return traffic)
|
||||
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
|
||||
# ============================================================
|
||||
# Loopback interface - always allow
|
||||
# ============================================================
|
||||
|
||||
-A INPUT -i lo -j ACCEPT
|
||||
|
||||
# ============================================================
|
||||
# Trusted networks - customize for your environment
|
||||
# ============================================================
|
||||
|
||||
# Example: Allow from specific interface (VPN, etc.)
|
||||
# -A INPUT -i tun0 -j ACCEPT
|
||||
|
||||
# Example: Allow from local network
|
||||
# -A INPUT -s 192.168.1.0/24 -j ACCEPT
|
||||
|
||||
# ============================================================
|
||||
# ICMP - allow ping for diagnostics
|
||||
# ============================================================
|
||||
|
||||
-A INPUT -p icmp --icmp-type echo-request -j ACCEPT
|
||||
-A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
|
||||
|
||||
# ============================================================
|
||||
# Default deny - drop everything not explicitly allowed
|
||||
# ============================================================
|
||||
|
||||
-A INPUT -j DROP
|
||||
|
||||
COMMIT
|
||||
5
portage/env/clang
vendored
Normal file
5
portage/env/clang
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Use clang/clang++ instead of GCC
|
||||
# Workaround for GCC 15 ICE (internal compiler error) with -march=znver3
|
||||
# and C++26 modules on certain packages
|
||||
CC="clang"
|
||||
CXX="clang++"
|
||||
3
portage/env/reduced
vendored
Normal file
3
portage/env/reduced
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Reduced parallelism for memory-hungry builds
|
||||
MAKEOPTS="-j6"
|
||||
NINJAOPTS="-j6"
|
||||
55
portage/make.conf
Normal file
55
portage/make.conf
Normal file
@ -0,0 +1,55 @@
|
||||
# make.conf for Legion S7 15ACH6
|
||||
# AMD Ryzen 9 5900HX with Radeon Graphics (16) @ 4.683GHz
|
||||
# AMD ATI Radeon Vega Series / Radeon Vega Mobile Series
|
||||
# NVIDIA GeForce RTX 3050 Ti Mobile / Max-Q
|
||||
|
||||
# Compiler Settings
|
||||
COMMON_FLAGS="-march=znver3 -O2 -pipe"
|
||||
CFLAGS="${COMMON_FLAGS}"
|
||||
CXXFLAGS="${COMMON_FLAGS}"
|
||||
FCFLAGS="${COMMON_FLAGS}"
|
||||
FFLAGS="${COMMON_FLAGS}"
|
||||
CHOST="x86_64-pc-linux-gnu"
|
||||
|
||||
# Build Settings
|
||||
MAKEOPTS="-j16 -l14"
|
||||
EMERGE_DEFAULT_OPTS="--jobs=2 --load-average=14 --autounmask-write"
|
||||
|
||||
# Mirrors and Boot
|
||||
GENTOO_MIRRORS="https://mirrors.rit.edu/gentoo/ https://gentoo.osuosl.org/"
|
||||
GRUB_PLATFORMS="efi-64"
|
||||
|
||||
# Portage Features
|
||||
FEATURES="ccache parallel-fetch candy"
|
||||
CCACHE_DIR="/var/cache/ccache"
|
||||
|
||||
# Cleaner build output
|
||||
LC_MESSAGES=C
|
||||
|
||||
# Hardware
|
||||
VIDEO_CARDS="amdgpu nvidia"
|
||||
INPUT_DEVICES="libinput"
|
||||
|
||||
# USE Flags - Optimized for Hyprland Desktop
|
||||
|
||||
# Core system
|
||||
USE="btrfs wayland X opengl vulkan egl gbm"
|
||||
# Graphics - hybrid AMD iGPU + NVIDIA dGPU
|
||||
USE="${USE} nvidia amdgpu opencl vaapi"
|
||||
# Audio - PipeWire only
|
||||
USE="${USE} pipewire alsa"
|
||||
# Network and connectivity
|
||||
USE="${USE} networkmanager bluetooth wifi"
|
||||
# Desktop environment essentials
|
||||
USE="${USE} udisks cups nls threads ssl crypt zstd"
|
||||
# GUI toolkits and libraries
|
||||
USE="${USE} gtk qt5 qt6 ffmpeg webp libnotify harfbuzz icu"
|
||||
# Wayland/Hyprland specific
|
||||
USE="${USE} screencast udev"
|
||||
# Kernel
|
||||
USE="${USE} dist-kernel"
|
||||
# Firmware (explicit for linux-firmware)
|
||||
USE="${USE} redistributable"
|
||||
|
||||
# License Acceptance
|
||||
ACCEPT_LICENSE="linux-fw-redistributable all-rights-reserved NVIDIA-* ValveSteamLicense BUSL-1.1"
|
||||
2
portage/package.accept_keywords/gvfs
Normal file
2
portage/package.accept_keywords/gvfs
Normal file
@ -0,0 +1,2 @@
|
||||
# GVFS virtual filesystem
|
||||
gnome-base/gvfs ~amd64
|
||||
104
portage/package.accept_keywords/hyprland
Normal file
104
portage/package.accept_keywords/hyprland
Normal file
@ -0,0 +1,104 @@
|
||||
# Hyprland ecosystem packages requiring ~amd64
|
||||
# Verified against packages.gentoo.org on 2026-01-13
|
||||
|
||||
# =============================================================================
|
||||
# GURU OVERLAY PACKAGES
|
||||
# =============================================================================
|
||||
|
||||
# Hyprland core and libraries
|
||||
gui-wm/hyprland ~amd64
|
||||
gui-libs/aquamarine ~amd64
|
||||
gui-libs/hyprutils ~amd64
|
||||
gui-libs/hyprtoolkit ~amd64
|
||||
gui-libs/hyprwire ~amd64
|
||||
gui-libs/hyprland-guiutils ~amd64
|
||||
dev-libs/hyprlang ~amd64
|
||||
dev-libs/hyprgraphics ~amd64
|
||||
dev-util/hyprwayland-scanner ~amd64
|
||||
|
||||
# Hyprland apps (GURU)
|
||||
gui-apps/hypridle ~amd64
|
||||
gui-apps/hyprlock ~amd64
|
||||
gui-apps/hyprpaper ~amd64
|
||||
gui-apps/hyprsunset ~amd64
|
||||
gui-apps/quickshell ~amd64
|
||||
gui-apps/hyprpicker ~amd64
|
||||
gui-libs/hyprcursor ~amd64
|
||||
sys-auth/hyprpolkitagent ~amd64
|
||||
|
||||
# Wayland utilities (GURU)
|
||||
gui-apps/swaync ~amd64
|
||||
gui-apps/awww ~amd64
|
||||
gui-apps/wlogout ~amd64
|
||||
gui-apps/nwg-displays ~amd64
|
||||
app-misc/nwg-look ~amd64
|
||||
gui-apps/rofi-wayland ~amd64
|
||||
|
||||
# Clipboard (GURU)
|
||||
app-misc/cliphist ~amd64
|
||||
|
||||
# Theming (GURU)
|
||||
x11-themes/kvantum ~amd64
|
||||
x11-themes/qogir-icon-theme ~amd64
|
||||
x11-themes/bibata-cursor ~amd64
|
||||
x11-themes/gtk-engines-murrine ~amd64
|
||||
|
||||
# D-Bus library (GURU)
|
||||
dev-cpp/sdbus-c++ ~amd64
|
||||
|
||||
# GTK4 layer shell (GURU)
|
||||
gui-libs/gtk4-layer-shell ~amd64
|
||||
|
||||
# Audio (GURU)
|
||||
media-sound/pamixer ~amd64
|
||||
|
||||
# Wallpaper tools (GURU)
|
||||
x11-misc/wallust ~amd64
|
||||
|
||||
# XDG portals (GURU)
|
||||
gui-libs/xdg-desktop-portal-hyprland ~amd64
|
||||
# Note: sys-apps/xdg-desktop-portal-gtk is stable in main tree, no keyword needed
|
||||
|
||||
# Brightness control (GURU)
|
||||
app-misc/brightnessctl ~amd64
|
||||
|
||||
# Fonts (GURU)
|
||||
media-fonts/nerdfonts ~amd64
|
||||
media-fonts/fontawesome ~amd64
|
||||
|
||||
# Media plugins (GURU)
|
||||
mpv-plugin/mpv-mpris ~amd64
|
||||
|
||||
# NVIDIA VA-API (GURU)
|
||||
media-libs/nvidia-vaapi-driver ~amd64
|
||||
|
||||
# Dialogs (GURU)
|
||||
gnome-extra/yad ~amd64
|
||||
|
||||
# Shell plugins (GURU)
|
||||
app-shells/zsh-autosuggestions ~amd64
|
||||
|
||||
# Libraries
|
||||
# Note: dev-util/umockdev is stable in main tree, no keyword needed
|
||||
# Note: dev-libs/libdbusmenu is stable in main tree, no keyword needed
|
||||
|
||||
# Utilities (GURU)
|
||||
app-misc/bc ~amd64
|
||||
|
||||
# =============================================================================
|
||||
# MAIN TREE PACKAGES (testing only, no stable version)
|
||||
# =============================================================================
|
||||
|
||||
# Shell utilities (main tree, ~amd64 only)
|
||||
app-shells/zoxide ~amd64
|
||||
|
||||
# Image viewer (main tree, ~amd64 only)
|
||||
media-gfx/loupe ~amd64
|
||||
|
||||
# Build dependencies
|
||||
dev-cpp/glaze ~amd64
|
||||
dev-util/breakpad ~amd64
|
||||
dev-libs/linux-syscall-support ~amd64
|
||||
dev-embedded/libdisasm ~amd64
|
||||
gui-apps/wlr-randr ~amd64
|
||||
x11-apps/xcur2png ~amd64
|
||||
2
portage/package.accept_keywords/jetbrains
Normal file
2
portage/package.accept_keywords/jetbrains
Normal file
@ -0,0 +1,2 @@
|
||||
# JetBrains Toolbox
|
||||
dev-util/jetbrains-toolbox ~amd64
|
||||
3
portage/package.accept_keywords/nvidia
Normal file
3
portage/package.accept_keywords/nvidia
Normal file
@ -0,0 +1,3 @@
|
||||
# NVIDIA drivers and EGL Wayland
|
||||
x11-drivers/nvidia-drivers ~amd64
|
||||
gui-libs/egl-wayland2 ~amd64
|
||||
3
portage/package.accept_keywords/podman
Normal file
3
portage/package.accept_keywords/podman
Normal file
@ -0,0 +1,3 @@
|
||||
# Podman container tools
|
||||
app-containers/podman-compose ~amd64
|
||||
app-containers/podman-tui ~amd64
|
||||
4
portage/package.accept_keywords/steam
Normal file
4
portage/package.accept_keywords/steam
Normal file
@ -0,0 +1,4 @@
|
||||
# Steam overlay
|
||||
*/*::steam-overlay
|
||||
games-util/game-device-udev-rules
|
||||
sys-libs/libudev-compat
|
||||
3
portage/package.accept_keywords/udisks
Normal file
3
portage/package.accept_keywords/udisks
Normal file
@ -0,0 +1,3 @@
|
||||
# UDisks disk management
|
||||
sys-fs/udisks ~amd64
|
||||
sys-libs/libblockdev ~amd64
|
||||
1
portage/package.env/clang
Normal file
1
portage/package.env/clang
Normal file
@ -0,0 +1 @@
|
||||
llvm-core/clang reduced
|
||||
1
portage/package.env/hyprland
Normal file
1
portage/package.env/hyprland
Normal file
@ -0,0 +1 @@
|
||||
gui-wm/hyprland clang
|
||||
1
portage/package.env/llvm
Normal file
1
portage/package.env/llvm
Normal file
@ -0,0 +1 @@
|
||||
llvm-core/llvm reduced
|
||||
2
portage/package.env/webkit
Normal file
2
portage/package.env/webkit
Normal file
@ -0,0 +1,2 @@
|
||||
# webkit-gtk is extremely memory-hungry
|
||||
net-libs/webkit-gtk reduced
|
||||
10
portage/package.use/circular-dependencies
Normal file
10
portage/package.use/circular-dependencies
Normal file
@ -0,0 +1,10 @@
|
||||
# Circular dependency workarounds for initial @world compile
|
||||
#
|
||||
# IMPORTANT: After @world compiles successfully, you should:
|
||||
# Run: emerge -1 media-libs/freetype media-libs/harfbuzz
|
||||
#
|
||||
# This ensures both packages are built with full feature support.
|
||||
|
||||
dev-python/pillow -truetype
|
||||
media-libs/libwebp -tiff
|
||||
media-libs/freetype -harfbuzz
|
||||
2
portage/package.use/gvfs
Normal file
2
portage/package.use/gvfs
Normal file
@ -0,0 +1,2 @@
|
||||
# GVFS - FUSE support for user-space mounts
|
||||
gnome-base/gvfs fuse
|
||||
33
portage/package.use/initial-use-flags
Normal file
33
portage/package.use/initial-use-flags
Normal file
@ -0,0 +1,33 @@
|
||||
# Package-specific USE flags for Hyprland desktop
|
||||
|
||||
# Python target for nwg-displays
|
||||
gui-apps/nwg-displays python_targets_python3_12
|
||||
|
||||
# GTK layer shell for Wayland
|
||||
gui-libs/gtk-layer-shell introspection vala
|
||||
|
||||
# NerdFonts variants to install
|
||||
media-fonts/nerdfonts firacode jetbrainsmono fantasque sourcecodepro hack
|
||||
|
||||
# PipeWire extras
|
||||
media-video/pipewire ffmpeg extra
|
||||
|
||||
# Thunar panel integration
|
||||
xfce-base/thunar-volman udisks
|
||||
|
||||
# MTP support for phones/devices
|
||||
gnome-base/gvfs mtp
|
||||
|
||||
# QuickShell Qt6 support
|
||||
dev-qt/qt5compat qml
|
||||
|
||||
# NVIDIA driver options
|
||||
x11-drivers/nvidia-drivers modules-sign persistenced
|
||||
|
||||
# Legion laptop hardware control (fan curves, power management)
|
||||
sys-firmware/lenovolegionlinux gui elogind
|
||||
|
||||
# Linux firmware redistributable blobs
|
||||
# Prevent linux-firmware from pulling in gentoo-kernel before @world is compiled
|
||||
# The global dist-kernel USE flag would otherwise trigger kernel installation
|
||||
sys-kernel/linux-firmware redistributable -dist-kernel
|
||||
2
portage/package.use/iptables
Normal file
2
portage/package.use/iptables
Normal file
@ -0,0 +1,2 @@
|
||||
# Required by podman for container networking
|
||||
net-firewall/iptables nftables
|
||||
4
portage/package.use/llvm
Normal file
4
portage/package.use/llvm
Normal file
@ -0,0 +1,4 @@
|
||||
# Build LLVM/clang with clang instead of GCC
|
||||
# Workaround for GCC 15 ICE on AMDGPURewriteAGPRCopyMFMA.cpp
|
||||
llvm-core/llvm clang
|
||||
llvm-core/clang clang
|
||||
2
portage/package.use/nodejs
Normal file
2
portage/package.use/nodejs
Normal file
@ -0,0 +1,2 @@
|
||||
# Node.js - include npm package manager
|
||||
net-libs/nodejs npm
|
||||
2
portage/package.use/podman
Normal file
2
portage/package.use/podman
Normal file
@ -0,0 +1,2 @@
|
||||
# Podman container runtime
|
||||
app-containers/podman wrapper
|
||||
140
portage/package.use/steam
Normal file
140
portage/package.use/steam
Normal file
@ -0,0 +1,140 @@
|
||||
# Steam 32-bit dependencies
|
||||
app-accessibility/at-spi2-core abi_x86_32
|
||||
app-arch/bzip2 abi_x86_32
|
||||
app-arch/lz4 abi_x86_32
|
||||
app-arch/xz-utils abi_x86_32
|
||||
app-arch/zstd abi_x86_32
|
||||
app-crypt/p11-kit abi_x86_32
|
||||
dev-db/sqlite abi_x86_32
|
||||
dev-lang/rust-bin abi_x86_32
|
||||
dev-libs/dbus-glib abi_x86_32
|
||||
dev-libs/elfutils abi_x86_32
|
||||
dev-libs/expat abi_x86_32
|
||||
dev-libs/fribidi abi_x86_32
|
||||
dev-libs/glib abi_x86_32
|
||||
dev-libs/gmp abi_x86_32
|
||||
dev-libs/icu abi_x86_32
|
||||
dev-libs/json-glib abi_x86_32
|
||||
dev-libs/libevdev abi_x86_32
|
||||
dev-libs/libffi abi_x86_32
|
||||
dev-libs/libgcrypt abi_x86_32
|
||||
dev-libs/libgpg-error abi_x86_32
|
||||
dev-libs/libgudev abi_x86_32
|
||||
dev-libs/libgusb abi_x86_32
|
||||
dev-libs/libpcre2 abi_x86_32
|
||||
dev-libs/libtasn1 abi_x86_32
|
||||
dev-libs/libunistring abi_x86_32
|
||||
dev-libs/libusb abi_x86_32
|
||||
dev-libs/libxml2 abi_x86_32
|
||||
dev-libs/lzo abi_x86_32
|
||||
dev-libs/nettle abi_x86_32
|
||||
dev-libs/nspr abi_x86_32
|
||||
dev-libs/nss abi_x86_32
|
||||
dev-libs/openssl abi_x86_32
|
||||
dev-libs/wayland abi_x86_32
|
||||
dev-util/directx-headers abi_x86_32
|
||||
dev-util/spirv-tools abi_x86_32
|
||||
dev-util/sysprof-capture abi_x86_32
|
||||
gnome-base/librsvg abi_x86_32
|
||||
gui-libs/libdecor abi_x86_32
|
||||
llvm-core/clang abi_x86_32
|
||||
llvm-core/llvm abi_x86_32
|
||||
media-gfx/graphite2 abi_x86_32
|
||||
media-libs/alsa-lib abi_x86_32
|
||||
media-libs/flac abi_x86_32
|
||||
media-libs/fontconfig abi_x86_32
|
||||
media-libs/freetype abi_x86_32
|
||||
media-libs/glu abi_x86_32
|
||||
media-libs/harfbuzz abi_x86_32
|
||||
media-libs/lcms abi_x86_32
|
||||
media-libs/libepoxy abi_x86_32
|
||||
media-libs/libglvnd abi_x86_32
|
||||
media-libs/libjpeg-turbo abi_x86_32
|
||||
media-libs/libogg abi_x86_32
|
||||
media-libs/libpng abi_x86_32
|
||||
media-libs/libpulse abi_x86_32
|
||||
media-libs/libsdl2 abi_x86_32
|
||||
media-libs/libsndfile abi_x86_32
|
||||
media-libs/libva abi_x86_32
|
||||
media-libs/libvorbis abi_x86_32
|
||||
media-libs/libwebp abi_x86_32
|
||||
media-libs/mesa abi_x86_32 vulkan
|
||||
media-libs/openal abi_x86_32
|
||||
media-libs/opus abi_x86_32
|
||||
media-libs/tiff abi_x86_32
|
||||
media-libs/vulkan-loader abi_x86_32
|
||||
media-sound/lame abi_x86_32
|
||||
media-sound/mpg123-base abi_x86_32
|
||||
media-video/pipewire abi_x86_32
|
||||
net-dns/c-ares abi_x86_32
|
||||
net-dns/libidn2 abi_x86_32
|
||||
net-libs/gnutls abi_x86_32
|
||||
net-libs/libasyncns abi_x86_32
|
||||
net-libs/libndp abi_x86_32
|
||||
net-libs/libpsl abi_x86_32
|
||||
net-libs/nghttp2 abi_x86_32
|
||||
net-libs/nghttp3 abi_x86_32
|
||||
net-misc/curl abi_x86_32
|
||||
net-misc/networkmanager abi_x86_32
|
||||
net-print/cups abi_x86_32
|
||||
sys-apps/dbus abi_x86_32
|
||||
sys-apps/systemd-utils abi_x86_32
|
||||
sys-apps/util-linux abi_x86_32
|
||||
sys-libs/gdbm abi_x86_32
|
||||
sys-libs/gpm abi_x86_32
|
||||
sys-libs/libcap abi_x86_32
|
||||
sys-libs/libudev-compat abi_x86_32
|
||||
sys-libs/ncurses abi_x86_32
|
||||
sys-libs/pam abi_x86_32
|
||||
sys-libs/readline abi_x86_32
|
||||
sys-libs/zlib abi_x86_32
|
||||
virtual/glu abi_x86_32
|
||||
virtual/libelf abi_x86_32
|
||||
virtual/libiconv abi_x86_32
|
||||
virtual/libintl abi_x86_32
|
||||
virtual/libudev abi_x86_32
|
||||
virtual/libusb abi_x86_32
|
||||
virtual/opengl abi_x86_32
|
||||
virtual/rust abi_x86_32
|
||||
virtual/zlib abi_x86_32
|
||||
x11-libs/cairo abi_x86_32
|
||||
x11-libs/extest abi_x86_32
|
||||
x11-libs/gdk-pixbuf abi_x86_32
|
||||
x11-libs/gtk+ abi_x86_32
|
||||
x11-libs/libdrm abi_x86_32
|
||||
x11-libs/libICE abi_x86_32
|
||||
x11-libs/libpciaccess abi_x86_32
|
||||
x11-libs/libSM abi_x86_32
|
||||
x11-libs/libvdpau abi_x86_32
|
||||
x11-libs/libX11 abi_x86_32
|
||||
x11-libs/libXau abi_x86_32
|
||||
x11-libs/libxcb abi_x86_32
|
||||
x11-libs/libXcomposite abi_x86_32
|
||||
x11-libs/libXcursor abi_x86_32
|
||||
x11-libs/libXdamage abi_x86_32
|
||||
x11-libs/libXdmcp abi_x86_32
|
||||
x11-libs/libXext abi_x86_32
|
||||
x11-libs/libXfixes abi_x86_32
|
||||
x11-libs/libXft abi_x86_32
|
||||
x11-libs/libXi abi_x86_32
|
||||
x11-libs/libXinerama abi_x86_32
|
||||
x11-libs/libxkbcommon abi_x86_32
|
||||
x11-libs/libXrandr abi_x86_32
|
||||
x11-libs/libXrender abi_x86_32
|
||||
x11-libs/libXScrnSaver abi_x86_32
|
||||
x11-libs/libxshmfence abi_x86_32
|
||||
x11-libs/libXtst abi_x86_32
|
||||
x11-libs/libXxf86vm abi_x86_32
|
||||
x11-libs/pango abi_x86_32
|
||||
x11-libs/pixman abi_x86_32
|
||||
x11-libs/xcb-util-keysyms abi_x86_32
|
||||
x11-misc/colord abi_x86_32
|
||||
|
||||
# NVIDIA 32-bit support
|
||||
gui-libs/egl-gbm abi_x86_32
|
||||
gui-libs/egl-wayland abi_x86_32
|
||||
gui-libs/egl-x11 abi_x86_32
|
||||
x11-drivers/nvidia-drivers abi_x86_32
|
||||
|
||||
# Easy Anti-Cheat support
|
||||
sys-libs/glibc hash-sysv-compat
|
||||
2
portage/package.use/waybar
Normal file
2
portage/package.use/waybar
Normal file
@ -0,0 +1,2 @@
|
||||
# Waybar - status bar for Wayland
|
||||
gui-apps/waybar network tray mpris
|
||||
173
portage/sets/hyprland
Normal file
173
portage/sets/hyprland
Normal file
@ -0,0 +1,173 @@
|
||||
# Hyprland Desktop Set for Legion S7 15ACH6
|
||||
# Based on JaKooLit Arch-Hyprland dotfiles
|
||||
# For use with: emerge @hyprland
|
||||
|
||||
# =============================================================================
|
||||
# HYPRLAND CORE
|
||||
# =============================================================================
|
||||
gui-wm/hyprland
|
||||
gui-apps/hypridle
|
||||
gui-apps/hyprlock
|
||||
gui-apps/quickshell
|
||||
gui-libs/hyprcursor
|
||||
gui-apps/hyprsunset
|
||||
gui-apps/awww
|
||||
sys-auth/hyprpolkitagent
|
||||
gui-apps/hyprpicker
|
||||
|
||||
# =============================================================================
|
||||
# DESKTOP UTILITIES
|
||||
# =============================================================================
|
||||
sys-devel/bc
|
||||
app-misc/cliphist
|
||||
app-misc/jq
|
||||
gui-apps/grim
|
||||
gui-apps/slurp
|
||||
gui-apps/swappy
|
||||
gui-apps/swaync
|
||||
gui-apps/waybar
|
||||
gui-apps/wl-clipboard
|
||||
gui-apps/wlogout
|
||||
gui-apps/rofi-wayland
|
||||
x11-misc/wallust
|
||||
|
||||
# =============================================================================
|
||||
# QT/GTK THEMING
|
||||
# =============================================================================
|
||||
dev-qt/qt5compat
|
||||
dev-qt/qtsvg
|
||||
dev-qt/qtdeclarative
|
||||
gui-apps/qt6ct
|
||||
x11-misc/qt5ct
|
||||
x11-themes/kvantum
|
||||
|
||||
# =============================================================================
|
||||
# SYSTEM UTILITIES
|
||||
# =============================================================================
|
||||
gnome-base/gvfs
|
||||
gnome-extra/nm-applet
|
||||
gnome-extra/polkit-gnome
|
||||
gnome-extra/yad
|
||||
media-gfx/imagemagick
|
||||
media-libs/libspng
|
||||
sys-apps/inxi
|
||||
x11-misc/xdg-user-dirs
|
||||
x11-misc/xdg-utils
|
||||
|
||||
# =============================================================================
|
||||
# AUDIO
|
||||
# =============================================================================
|
||||
media-sound/pamixer
|
||||
media-sound/pavucontrol
|
||||
media-sound/playerctl
|
||||
|
||||
# =============================================================================
|
||||
# PYTHON DEPENDENCIES
|
||||
# =============================================================================
|
||||
dev-python/pyquery
|
||||
dev-python/requests
|
||||
dev-python/beautifulsoup4
|
||||
dev-python/pygments
|
||||
dev-python/pyyaml
|
||||
dev-python/secretstorage
|
||||
dev-python/uv
|
||||
|
||||
# =============================================================================
|
||||
# TERMINAL & TOOLS
|
||||
# =============================================================================
|
||||
app-arch/unzip
|
||||
app-arch/xarchiver
|
||||
net-misc/curl
|
||||
net-misc/wget
|
||||
x11-terms/kitty
|
||||
|
||||
# =============================================================================
|
||||
# MONITORING
|
||||
# =============================================================================
|
||||
app-misc/fastfetch
|
||||
media-sound/cava
|
||||
sys-process/btop
|
||||
sys-process/htop
|
||||
sys-process/nvtop
|
||||
|
||||
# =============================================================================
|
||||
# MEDIA
|
||||
# =============================================================================
|
||||
media-video/mpv
|
||||
mpv-plugin/mpv-mpris
|
||||
media-video/ffmpegthumbnailer
|
||||
net-misc/yt-dlp
|
||||
|
||||
# =============================================================================
|
||||
# DESKTOP EXTRAS
|
||||
# =============================================================================
|
||||
app-editors/mousepad
|
||||
app-text/evince
|
||||
gnome-extra/gnome-system-monitor
|
||||
gui-apps/nwg-displays
|
||||
app-misc/nwg-look
|
||||
media-gfx/loupe
|
||||
sci-calculators/qalculate-gtk
|
||||
app-misc/brightnessctl
|
||||
sys-power/upower
|
||||
|
||||
# =============================================================================
|
||||
# FILE MANAGER
|
||||
# =============================================================================
|
||||
gnome-base/nautilus
|
||||
xfce-base/tumbler
|
||||
|
||||
# =============================================================================
|
||||
# FONTS
|
||||
# =============================================================================
|
||||
media-fonts/fantasque-sans-mono
|
||||
media-fonts/fira-code
|
||||
media-fonts/fontawesome
|
||||
media-fonts/jetbrains-mono
|
||||
media-fonts/nerdfonts
|
||||
media-fonts/noto
|
||||
media-fonts/noto-emoji
|
||||
|
||||
# =============================================================================
|
||||
# THEMES & ICONS
|
||||
# =============================================================================
|
||||
lxde-base/lxappearance
|
||||
x11-themes/gtk-engines-murrine
|
||||
x11-libs/gtk+:3
|
||||
x11-themes/adwaita-qt
|
||||
x11-themes/bibata-cursor
|
||||
x11-themes/gtk-engines
|
||||
x11-themes/papirus-icon-theme
|
||||
x11-themes/qogir-icon-theme
|
||||
|
||||
# =============================================================================
|
||||
# XDG PORTALS
|
||||
# =============================================================================
|
||||
sys-apps/xdg-desktop-portal-gtk
|
||||
gui-libs/xdg-desktop-portal-hyprland
|
||||
dev-util/umockdev
|
||||
|
||||
# =============================================================================
|
||||
# ADDITIONAL DEV/LIBS
|
||||
# =============================================================================
|
||||
dev-build/meson
|
||||
dev-libs/gjs
|
||||
dev-libs/glib
|
||||
dev-libs/gobject-introspection
|
||||
gnome-base/gnome-keyring
|
||||
net-libs/libsoup:3.0
|
||||
net-libs/nodejs
|
||||
dev-libs/libdbusmenu
|
||||
# Note: sddm is installed via services.sh
|
||||
|
||||
# =============================================================================
|
||||
# GRAPHICS (NVIDIA/AMD HYBRID)
|
||||
# =============================================================================
|
||||
media-libs/libva
|
||||
media-libs/nvidia-vaapi-driver
|
||||
|
||||
# =============================================================================
|
||||
# POWER MANAGEMENT & FIRMWARE
|
||||
# =============================================================================
|
||||
sys-power/cpupower
|
||||
sys-apps/fwupd
|
||||
410
procedure.md
Normal file
410
procedure.md
Normal file
@ -0,0 +1,410 @@
|
||||
# Gentoo Installation Procedure - Legion Laptop
|
||||
|
||||
Automated installation using Python scripts for Lenovo Legion laptops with AMD + NVIDIA hybrid graphics.
|
||||
|
||||
## Hardware
|
||||
|
||||
Tested on Legion S7 15ACH6:
|
||||
- **CPU**: AMD Ryzen 9 5900HX (16 threads, Zen 3)
|
||||
- **iGPU**: AMD Radeon Vega (Cezanne)
|
||||
- **dGPU**: NVIDIA GeForce RTX 3050 Ti Mobile
|
||||
- **RAM**: 24GB DDR4
|
||||
- **Storage**: NVMe with LUKS2 encryption
|
||||
|
||||
Should work on other Legion models with similar hardware.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Two commands handle the entire installation:
|
||||
|
||||
```bash
|
||||
# Phase 1: Live environment -> chroot -> first reboot
|
||||
python setup.py --install # Run once outside chroot, once inside
|
||||
|
||||
# Phase 2: After first boot
|
||||
python setup.py --desktop # Installs Hyprland + optional fingerprint
|
||||
```
|
||||
|
||||
`--install` auto-detects whether you're in the live environment or chroot:
|
||||
- **Outside chroot**: Runs disk → stage3 → config → fstab → chroot
|
||||
- **Inside chroot**: Runs sync → world → firmware → kernel → services → users → nvidia → bootloader
|
||||
|
||||
### Idempotency
|
||||
|
||||
**All commands are safe to re-run.** If interrupted or if something fails:
|
||||
- Simply run the same command again
|
||||
- Completed steps are detected and skipped
|
||||
- Partial state is handled gracefully (e.g., LUKS exists but not mounted)
|
||||
|
||||
Example: If `world` fails mid-compile, just run `python setup.py --install` again.
|
||||
|
||||
---
|
||||
|
||||
## Pre-Installation
|
||||
|
||||
### 1. Prepare LUKS Passphrase
|
||||
|
||||
Generate a strong passphrase for disk encryption:
|
||||
|
||||
```bash
|
||||
# Generate 56-char alphanumeric passphrase
|
||||
openssl rand -base64 64 | tr -dc 'A-Za-z0-9' | head -c 56
|
||||
```
|
||||
|
||||
Store securely (password manager, hardware key, offline backup).
|
||||
|
||||
### 2. Boot Live Environment
|
||||
|
||||
At GRUB menu, press `e`, add `video=1920x1080` to the linux line (HiDPI fix), then `Ctrl+X`.
|
||||
|
||||
```bash
|
||||
# Configure network
|
||||
net-setup
|
||||
|
||||
# Verify DNS
|
||||
ping -c 2 gentoo.org
|
||||
```
|
||||
|
||||
### 3. Clone Repository
|
||||
|
||||
```bash
|
||||
emerge --ask dev-vcs/git
|
||||
git clone https://github.com/<your-username>/gentoo-legion-python /root/gentoo
|
||||
cd /root/gentoo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stage 1: Disk Setup (Pre-Chroot)
|
||||
|
||||
Run from live environment:
|
||||
|
||||
```bash
|
||||
python setup.py
|
||||
```
|
||||
|
||||
Select commands in order, or run individually:
|
||||
|
||||
### 1) disk - Partition, Encrypt, Mount
|
||||
|
||||
- Creates GPT partition table
|
||||
- EFI partition (1GB)
|
||||
- Swap partition (24GB)
|
||||
- LUKS2 encrypted root (remaining space)
|
||||
- Btrfs with subvolumes (@, @home, @var, @log, @snapshots)
|
||||
- Mounts everything to /mnt/gentoo
|
||||
|
||||
**Re-running**: If LUKS already exists, you'll be prompted to `reformat` or `reuse`. Select `reuse` to skip destructive steps and just mount the existing setup.
|
||||
|
||||
### 2) stage3 - Download & Extract
|
||||
|
||||
- Fetches latest stage3-amd64-openrc tarball
|
||||
- Verifies checksum
|
||||
- Extracts to /mnt/gentoo
|
||||
|
||||
### 3) config - Copy Portage Configuration
|
||||
|
||||
Copies from `portage/` to `/mnt/gentoo/etc/portage/`:
|
||||
- make.conf
|
||||
- package.use/
|
||||
- package.accept_keywords/
|
||||
- package.env/
|
||||
- package.license/
|
||||
- env/
|
||||
- sets/
|
||||
|
||||
Also copies from repo root to `/mnt/gentoo/etc/`:
|
||||
- dracut.conf.d/
|
||||
|
||||
### 4) fstab - Generate Filesystem Config
|
||||
|
||||
Generates:
|
||||
- /etc/fstab (EFI, Btrfs subvolumes)
|
||||
- /etc/conf.d/dmcrypt (encrypted swap)
|
||||
|
||||
### 5) chroot - Prepare & Enter
|
||||
|
||||
- Copies resolv.conf for network
|
||||
- Mounts /proc, /sys, /dev, /run
|
||||
- Enters chroot
|
||||
|
||||
**Or run all pre-chroot steps:**
|
||||
```bash
|
||||
python setup.py all
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stage 2: Base System (Inside Chroot)
|
||||
|
||||
After entering chroot, clone the repo:
|
||||
|
||||
```bash
|
||||
# Sync portage first (needed to install anything)
|
||||
emerge --sync
|
||||
|
||||
# Install git and clone repo
|
||||
emerge --ask dev-vcs/git
|
||||
git clone https://github.com/<your-username>/gentoo-legion-python /root/gentoo
|
||||
cd /root/gentoo
|
||||
python setup.py --install
|
||||
```
|
||||
|
||||
The `--install` command will pick up where it left off (inside chroot it runs sync → world → ... → bootloader).
|
||||
|
||||
### 7) sync - Portage Sync & Profile
|
||||
|
||||
- Syncs portage tree (webrsync)
|
||||
- Sets profile: default/linux/amd64/23.0/desktop
|
||||
- Enables GURU overlay (required for Hyprland)
|
||||
- Sets timezone (America/New_York)
|
||||
- Configures locale (en_US.UTF-8)
|
||||
- Sets hostname
|
||||
- Activates encrypted swap for builds
|
||||
|
||||
Note: ccache is enabled automatically via `FEATURES="ccache"` in make.conf.
|
||||
|
||||
### 8) world - Update @world
|
||||
|
||||
```bash
|
||||
emerge --ask --verbose --update --deep --newuse @world
|
||||
```
|
||||
|
||||
This takes several hours. Swap is active to prevent OOM.
|
||||
|
||||
**Circular Dependencies:** After @world completes, the script automatically:
|
||||
1. Reads `package.use/circular-dependencies` for temporarily disabled USE flags
|
||||
2. Clears the file (keeps header comments)
|
||||
3. Rebuilds affected packages with full USE flags (freetype, harfbuzz, libwebp, pillow)
|
||||
|
||||
### 9) firmware - Install Linux Firmware
|
||||
|
||||
```bash
|
||||
emerge sys-kernel/linux-firmware
|
||||
```
|
||||
|
||||
### 10) kernel - Install Kernel
|
||||
|
||||
Choose:
|
||||
1. `gentoo-kernel-bin` - Precompiled, fastest
|
||||
2. `gentoo-kernel` - Compiled locally
|
||||
|
||||
Dracut auto-generates initramfs with crypt + NVIDIA modules.
|
||||
|
||||
---
|
||||
|
||||
## Stage 3: Services & Configuration
|
||||
|
||||
### 11) services - System Services
|
||||
|
||||
Installs and configures:
|
||||
- **System**: sysklogd, cronie
|
||||
- **Network**: NetworkManager, chrony, iptables
|
||||
- **Bluetooth**: bluez, blueman
|
||||
- **Desktop**: udisks, power-profiles-daemon, rtkit
|
||||
- **Display Manager**: SDDM
|
||||
- **Containers**: podman, podman-compose
|
||||
- **Backup**: snapper
|
||||
|
||||
Configures OpenRC runlevels:
|
||||
- **boot**: dbus, elogind, dmcrypt
|
||||
- **default**: All services listed above
|
||||
|
||||
Copies from repo:
|
||||
- `conf.d/` → `/etc/conf.d/` (dmcrypt, iptables, snapper configs)
|
||||
- `iptables/` → `/var/lib/iptables/rules-save` and `/var/lib/ip6tables/rules-save`
|
||||
- `udev/` → `/etc/udev/rules.d/` (power profile rules)
|
||||
|
||||
### 12) users - Shell & User Setup
|
||||
|
||||
Installs shell tools:
|
||||
- zsh, zsh-autosuggestions, zsh-syntax-highlighting
|
||||
- starship, zoxide, fzf, lsd
|
||||
- gentoolkit, portage-utils, eix
|
||||
|
||||
Then:
|
||||
1. Copies .zshrc and starship.toml to /root
|
||||
2. Sets root shell to zsh
|
||||
3. Prompts for root password
|
||||
4. Configures sudo (enables wheel group)
|
||||
5. Creates user with groups: users, wheel, input
|
||||
6. Copies shell config to user home
|
||||
7. Prompts for user password
|
||||
|
||||
---
|
||||
|
||||
## Stage 4: Graphics & Bootloader
|
||||
|
||||
### 13) nvidia - NVIDIA Drivers
|
||||
|
||||
1. Installs nvidia-drivers
|
||||
2. Blacklists nouveau (`/etc/modprobe.d/blacklist-nouveau.conf`)
|
||||
3. Configures DRM modeset (`/etc/modprobe.d/nvidia.conf`)
|
||||
4. Copies dracut nvidia.conf for early KMS
|
||||
5. Optionally rebuilds initramfs
|
||||
6. Verifies NVIDIA modules in initramfs
|
||||
|
||||
### 14) bootloader - GRUB Installation
|
||||
|
||||
1. Copies dracut crypt.conf for LUKS support
|
||||
2. Installs GRUB
|
||||
3. Installs to EFI partition (--bootloader-id=Gentoo)
|
||||
4. Configures /etc/default/grub:
|
||||
- `GRUB_CMDLINE_LINUX_DEFAULT="nvidia_drm.modeset=1 acpi_backlight=native"`
|
||||
- `GRUB_ENABLE_CRYPTODISK=y`
|
||||
- `GRUB_GFXMODE=1920x1080` (HiDPI fix)
|
||||
5. Generates grub.cfg
|
||||
6. **Verifies initramfs** - checks for crypt/LUKS and NVIDIA modules
|
||||
|
||||
---
|
||||
|
||||
## First Boot
|
||||
|
||||
After GRUB, you'll be prompted for your LUKS passphrase, then login as root.
|
||||
|
||||
### Connect to WiFi
|
||||
|
||||
NetworkManager has no saved connections yet. Use the TUI:
|
||||
|
||||
```bash
|
||||
nmtui
|
||||
```
|
||||
|
||||
Select **Activate a connection** → choose your network → enter password.
|
||||
|
||||
Or via command line:
|
||||
|
||||
```bash
|
||||
nmcli device wifi list
|
||||
nmcli device wifi connect "YourSSID" password "YourPassword"
|
||||
```
|
||||
|
||||
Verify connectivity:
|
||||
|
||||
```bash
|
||||
ping -c 2 gentoo.org
|
||||
```
|
||||
|
||||
### Clone Repository (as user)
|
||||
|
||||
Switch to your user account and clone the repo:
|
||||
|
||||
```bash
|
||||
su - <username>
|
||||
git clone https://github.com/<your-username>/gentoo-legion-python ~/gentoo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stage 5: Desktop Environment
|
||||
|
||||
### Install Hyprland Desktop
|
||||
|
||||
As your user (not root):
|
||||
|
||||
```bash
|
||||
sudo emerge --ask @hyprland
|
||||
```
|
||||
|
||||
This installs:
|
||||
- Hyprland compositor
|
||||
- Waybar, swaync, rofi, wlogout
|
||||
- Qt/GTK theming (Kvantum, qt5ct, qt6ct)
|
||||
- Fonts (Nerd Fonts, JetBrains Mono, etc.)
|
||||
- Media tools (mpv, ffmpeg)
|
||||
- File manager (Nautilus)
|
||||
- And more...
|
||||
|
||||
### Post-Install Configuration
|
||||
|
||||
Copy Hyprland configs for multi-monitor setup:
|
||||
```bash
|
||||
# GPU auto-detect (comment out explicit AQ_* settings)
|
||||
cp ~/gentoo/hypr/ENVariables.conf ~/.config/hypr/UserConfigs/
|
||||
|
||||
# Triple monitor layout (customize for your setup)
|
||||
cp ~/gentoo/hypr/monitors.conf ~/.config/hypr/
|
||||
```
|
||||
|
||||
### 15) fingerprint - Fingerprint Authentication (Optional)
|
||||
|
||||
```bash
|
||||
python setup.py fingerprint
|
||||
```
|
||||
|
||||
Sets up the Elan fingerprint reader (04f3:0c4b) for authentication:
|
||||
|
||||
1. Installs `fprintd` and `libfprint` packages
|
||||
2. Downloads Lenovo TOD driver (`libfprint-2-tod1-elan.so`)
|
||||
3. Enrolls fingerprints for user
|
||||
4. Configures PAM for SDDM and hyprlock
|
||||
|
||||
**Usage after setup:**
|
||||
- **SDDM/hyprlock**: Press Enter on empty password field to activate fingerprint
|
||||
- **Enroll more fingers**: `fprintd-enroll -f <finger> <username>`
|
||||
- **Test**: `fprintd-verify <username>`
|
||||
|
||||
**Note**: Fingerprint is configured as an alternative to password, not a replacement.
|
||||
|
||||
---
|
||||
|
||||
## Utilities
|
||||
|
||||
### Swap Management
|
||||
|
||||
```bash
|
||||
python setup.py swap-on # Activate encrypted swap
|
||||
python setup.py swap-off # Deactivate encrypted swap
|
||||
```
|
||||
|
||||
Swap auto-activates during `sync` for heavy builds.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Complete Installation Order
|
||||
|
||||
**Pre-chroot (live environment):**
|
||||
```
|
||||
disk → stage3 → config → fstab → chroot
|
||||
```
|
||||
|
||||
**Inside chroot:**
|
||||
```
|
||||
sync → world → firmware → kernel → services → users → nvidia → bootloader
|
||||
```
|
||||
|
||||
**After reboot:**
|
||||
```
|
||||
nmtui (connect WiFi) → su - <username> → emerge @hyprland → configure Hyprland
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Command failed mid-way**: Just run the same command again. All commands are idempotent and will skip completed steps.
|
||||
|
||||
**Build OOM**: Activate swap with `python setup.py swap-on`
|
||||
|
||||
**Initramfs missing modules**: Run `python setup.py bootloader` to verify
|
||||
|
||||
**NVIDIA not loading**: Check `/etc/modprobe.d/` configs, rebuild initramfs with `dracut --force`
|
||||
|
||||
**Firewall not active**: Verify `/var/lib/iptables/rules-save` exists
|
||||
|
||||
**USE flag / keyword changes**: Handled automatically. The installer uses `--autounmask-write` and auto-dispatches config changes on retry.
|
||||
|
||||
**Disk already partitioned**: The `disk` command detects existing LUKS and offers to reuse it instead of reformatting.
|
||||
|
||||
**WiFi not working after reboot**: Ensure `NetworkManager` service is running (`rc-service NetworkManager status`). Check `nmcli device` to see if the WiFi adapter is recognized.
|
||||
|
||||
### File Locations
|
||||
|
||||
| Config | Location |
|
||||
|--------|----------|
|
||||
| Portage | /etc/portage/ |
|
||||
| Dracut | /etc/dracut.conf.d/ |
|
||||
| Firewall rules | /var/lib/iptables/rules-save |
|
||||
| NVIDIA modprobe | /etc/modprobe.d/nvidia.conf |
|
||||
| GRUB | /etc/default/grub |
|
||||
| Shell config | ~/.zshrc, ~/.config/starship.toml |
|
||||
529
setup.py
Executable file
529
setup.py
Executable file
@ -0,0 +1,529 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Gentoo installation orchestrator for Legion S7 15ACH6.
|
||||
|
||||
Usage:
|
||||
python setup.py [command]
|
||||
python setup.py --install # Full install up to first reboot
|
||||
python setup.py --desktop # Post-reboot desktop setup
|
||||
|
||||
Major Phases:
|
||||
--install Full installation to first reboot (auto-detects chroot state)
|
||||
Outside chroot: disk -> stage3 -> config -> fstab -> chroot
|
||||
Inside chroot: sync -> world -> firmware -> kernel -> services
|
||||
-> users -> nvidia -> bootloader
|
||||
--desktop Post-reboot desktop setup (Hyprland + optional fingerprint)
|
||||
|
||||
Individual Commands:
|
||||
disk Partition, encrypt, and mount disks
|
||||
stage3 Download and extract stage3 tarball
|
||||
chroot Prepare and enter chroot environment
|
||||
config Copy portage and system configuration
|
||||
fstab Generate fstab, crypttab, and dmcrypt config
|
||||
sync Initial portage sync, profile, and overlays (inside chroot)
|
||||
world Update @world (inside chroot)
|
||||
firmware Install linux-firmware (inside chroot)
|
||||
kernel Install kernel (inside chroot)
|
||||
services Install and configure system services (inside chroot)
|
||||
users Configure shell, sudo, and create user (inside chroot)
|
||||
nvidia Install NVIDIA drivers and configure (inside chroot)
|
||||
bootloader Install GRUB and verify initramfs (inside chroot)
|
||||
fingerprint Set up fingerprint authentication (post-install)
|
||||
swap-on Activate encrypted swap (inside chroot)
|
||||
swap-off Deactivate encrypted swap (inside chroot)
|
||||
all Run pre-chroot steps (disk -> chroot)
|
||||
|
||||
Examples:
|
||||
python setup.py --install # Run full install phase (pre-reboot)
|
||||
python setup.py --desktop # Run desktop setup (post-reboot)
|
||||
python setup.py disk # Just partition disks
|
||||
python setup.py # Interactive menu
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from install.utils import info, success, error, fatal, check_root, check_uefi, prompt, emerge
|
||||
from install.disk import DiskConfig, DiskLayout, prepare_disk
|
||||
from install.stage3 import Stage3Config, fetch_stage3
|
||||
from install.chroot import prepare_chroot, enter_chroot
|
||||
from install.fstab import generate_all as generate_fstab_all
|
||||
from install.portage import copy_portage_config
|
||||
from install.sync import initial_sync, activate_swap, deactivate_swap
|
||||
from install.services import setup_services
|
||||
from install.users import setup_users
|
||||
from install.nvidia import setup_nvidia
|
||||
from install.bootloader import setup_bootloader
|
||||
|
||||
|
||||
# Path to config files (same directory as this script)
|
||||
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||
CONFIG_DIR = SCRIPT_DIR # Config files live alongside setup.py
|
||||
|
||||
|
||||
# Global state (set during disk preparation)
|
||||
_disk_layout: DiskLayout | None = None
|
||||
_disk_config: DiskConfig | None = None
|
||||
|
||||
|
||||
def cmd_disk() -> tuple[DiskLayout, DiskConfig]:
|
||||
"""Partition, encrypt, and mount disks."""
|
||||
global _disk_layout, _disk_config
|
||||
|
||||
check_uefi()
|
||||
layout, config = prepare_disk()
|
||||
|
||||
_disk_layout = layout
|
||||
_disk_config = config
|
||||
|
||||
return layout, config
|
||||
|
||||
|
||||
def cmd_stage3() -> None:
|
||||
"""Download and extract stage3."""
|
||||
config = Stage3Config(
|
||||
init_system="openrc",
|
||||
mount_root=Path("/mnt/gentoo"),
|
||||
)
|
||||
fetch_stage3(config)
|
||||
|
||||
|
||||
def cmd_config() -> None:
|
||||
"""Copy portage configuration."""
|
||||
copy_portage_config(
|
||||
source_dir=CONFIG_DIR,
|
||||
mount_root=Path("/mnt/gentoo"),
|
||||
)
|
||||
|
||||
|
||||
def cmd_fstab(layout: DiskLayout | None = None, config: DiskConfig | None = None) -> None:
|
||||
"""Generate filesystem configuration files."""
|
||||
if layout is None or config is None:
|
||||
fatal("Disk layout not available. Run 'disk' command first.")
|
||||
|
||||
generate_fstab_all(layout, config)
|
||||
|
||||
|
||||
def cmd_chroot() -> None:
|
||||
"""Prepare and enter chroot."""
|
||||
prepare_chroot()
|
||||
|
||||
print()
|
||||
response = prompt("Enter chroot now? (y/n): ")
|
||||
if response.lower() == "y":
|
||||
enter_chroot()
|
||||
|
||||
|
||||
def cmd_sync() -> None:
|
||||
"""Initial portage sync and profile setup (run inside chroot)."""
|
||||
initial_sync()
|
||||
|
||||
|
||||
def cmd_swap_on() -> None:
|
||||
"""Activate encrypted swap (inside chroot)."""
|
||||
activate_swap()
|
||||
|
||||
|
||||
def cmd_swap_off() -> None:
|
||||
"""Deactivate encrypted swap (inside chroot)."""
|
||||
deactivate_swap()
|
||||
|
||||
|
||||
def cmd_world() -> None:
|
||||
"""Update @world (inside chroot)."""
|
||||
info("=== Updating @world ===")
|
||||
emerge("@world", extra_args=["--update", "--deep", "--newuse"])
|
||||
success("@world update complete.")
|
||||
|
||||
# Handle circular dependencies - rebuild with full USE flags
|
||||
circ_deps_file = Path("/etc/portage/package.use/circular-dependencies")
|
||||
if circ_deps_file.exists():
|
||||
packages = parse_circular_deps(circ_deps_file)
|
||||
if packages:
|
||||
print()
|
||||
info("=== Rebuilding Circular Dependency Packages ===")
|
||||
info(f"Packages: {', '.join(packages)}")
|
||||
|
||||
# Clear the file (keep header)
|
||||
clear_circular_deps(circ_deps_file)
|
||||
|
||||
# Rebuild with full USE flags
|
||||
emerge(*packages, extra_args=["--oneshot"], ask=False)
|
||||
success("Circular dependency packages rebuilt with full USE flags.")
|
||||
|
||||
|
||||
def parse_circular_deps(filepath: Path) -> list[str]:
|
||||
"""Parse package names from circular-dependencies file."""
|
||||
packages = []
|
||||
for line in filepath.read_text().splitlines():
|
||||
line = line.strip()
|
||||
# Skip comments and empty lines
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
# Extract package name (first token before USE flags)
|
||||
parts = line.split()
|
||||
if parts:
|
||||
packages.append(parts[0])
|
||||
return packages
|
||||
|
||||
|
||||
def clear_circular_deps(filepath: Path) -> None:
|
||||
"""Clear circular-dependencies file, keeping header comments."""
|
||||
lines = filepath.read_text().splitlines()
|
||||
header = []
|
||||
for line in lines:
|
||||
# Keep comments and blank lines in header
|
||||
if line.startswith("#") or line.strip() == "":
|
||||
header.append(line)
|
||||
else:
|
||||
# Stop at first package line
|
||||
break
|
||||
# Write header, ensure trailing newline
|
||||
if header:
|
||||
filepath.write_text("\n".join(header) + "\n")
|
||||
else:
|
||||
filepath.write_text("")
|
||||
|
||||
|
||||
def cmd_firmware() -> None:
|
||||
"""Install linux-firmware (inside chroot)."""
|
||||
info("=== Installing Linux Firmware ===")
|
||||
emerge("sys-kernel/linux-firmware")
|
||||
success("Linux firmware installed.")
|
||||
|
||||
|
||||
def cmd_kernel() -> None:
|
||||
"""Install kernel (inside chroot)."""
|
||||
info("=== Installing Kernel ===")
|
||||
print()
|
||||
info("Options:")
|
||||
print(" 1) gentoo-kernel-bin - Precompiled, fastest")
|
||||
print(" 2) gentoo-kernel - Compiled locally with your USE flags")
|
||||
print()
|
||||
|
||||
choice = prompt("Select kernel [1]: ").strip()
|
||||
|
||||
if choice == "2":
|
||||
emerge("sys-kernel/gentoo-kernel")
|
||||
else:
|
||||
emerge("sys-kernel/gentoo-kernel-bin")
|
||||
|
||||
success("Kernel installed.")
|
||||
print()
|
||||
info("Kernel modules installed. Dracut will generate initramfs.")
|
||||
|
||||
|
||||
def cmd_services() -> None:
|
||||
"""Install and configure services (inside chroot)."""
|
||||
setup_services(source_dir=CONFIG_DIR)
|
||||
|
||||
|
||||
def cmd_users() -> None:
|
||||
"""Configure shell, sudo, and create user (inside chroot)."""
|
||||
setup_users(source_dir=CONFIG_DIR)
|
||||
|
||||
|
||||
def cmd_nvidia() -> None:
|
||||
"""Install NVIDIA drivers and configure (inside chroot)."""
|
||||
setup_nvidia(source_dir=CONFIG_DIR)
|
||||
|
||||
|
||||
def cmd_bootloader() -> None:
|
||||
"""Install GRUB and verify initramfs (inside chroot)."""
|
||||
setup_bootloader(source_dir=CONFIG_DIR)
|
||||
|
||||
|
||||
def cmd_fingerprint() -> None:
|
||||
"""Set up fingerprint authentication (post-install)."""
|
||||
from install.fingerprint import setup_fingerprint
|
||||
setup_fingerprint(source_dir=CONFIG_DIR)
|
||||
|
||||
|
||||
def is_in_chroot() -> bool:
|
||||
"""Detect if we're running inside chroot vs live environment."""
|
||||
# If /mnt/gentoo is mounted, we're in the live environment
|
||||
# If not, we're either in chroot or on the installed system
|
||||
from install.utils import is_mounted
|
||||
return not is_mounted(Path("/mnt/gentoo"))
|
||||
|
||||
|
||||
def cmd_install() -> None:
|
||||
"""Full installation up to first reboot."""
|
||||
if is_in_chroot():
|
||||
# Inside chroot: run sync through bootloader
|
||||
info("=== Install Phase (inside chroot) ===")
|
||||
print()
|
||||
info("Running: sync -> world -> firmware -> kernel -> services -> users -> nvidia -> bootloader")
|
||||
print()
|
||||
|
||||
cmd_sync()
|
||||
print()
|
||||
cmd_world()
|
||||
print()
|
||||
cmd_firmware()
|
||||
print()
|
||||
cmd_kernel()
|
||||
print()
|
||||
cmd_services()
|
||||
print()
|
||||
cmd_users()
|
||||
print()
|
||||
cmd_nvidia()
|
||||
print()
|
||||
cmd_bootloader()
|
||||
print()
|
||||
|
||||
success("=== Install phase complete ===")
|
||||
print()
|
||||
print("Next steps:")
|
||||
print(" 1. Exit chroot: exit")
|
||||
print(" 2. Unmount: umount -R /mnt/gentoo")
|
||||
print(" 3. Reboot: reboot")
|
||||
print(" 4. After reboot, run: python setup.py --desktop")
|
||||
else:
|
||||
# Outside chroot: run disk through chroot, then prompt
|
||||
info("=== Install Phase (pre-chroot) ===")
|
||||
print()
|
||||
|
||||
layout, config = cmd_disk()
|
||||
print()
|
||||
cmd_stage3()
|
||||
print()
|
||||
cmd_config()
|
||||
print()
|
||||
cmd_fstab(layout, config)
|
||||
print()
|
||||
cmd_chroot()
|
||||
|
||||
# After chroot exits, remind user
|
||||
print()
|
||||
success("=== Pre-chroot phase complete ===")
|
||||
print()
|
||||
print("You exited the chroot. To continue installation:")
|
||||
print(" 1. Re-enter chroot: chroot /mnt/gentoo /bin/bash")
|
||||
print(" 2. Source profile: source /etc/profile && export PS1='(chroot) $PS1'")
|
||||
print(" 3. Run: python /root/gentoo/v4/setup.py --install")
|
||||
|
||||
|
||||
def cmd_desktop() -> None:
|
||||
"""Post-reboot desktop setup."""
|
||||
info("=== Desktop Setup (post-reboot) ===")
|
||||
print()
|
||||
|
||||
# Install Hyprland desktop
|
||||
info("Installing Hyprland desktop environment...")
|
||||
emerge("@hyprland", extra_args=["--update", "--deep", "--newuse"])
|
||||
success("Hyprland desktop installed.")
|
||||
print()
|
||||
|
||||
# Offer fingerprint setup
|
||||
response = prompt("Set up fingerprint authentication? [y/N]: ").strip().lower()
|
||||
if response in ("y", "yes"):
|
||||
print()
|
||||
cmd_fingerprint()
|
||||
else:
|
||||
info("Skipped fingerprint setup. Run 'python setup.py fingerprint' later if needed.")
|
||||
|
||||
print()
|
||||
success("=== Desktop setup complete ===")
|
||||
print()
|
||||
print("Next steps:")
|
||||
print(" 1. Reboot to apply display manager")
|
||||
print(" 2. Log in via SDDM")
|
||||
print(" 3. Copy Hyprland configs:")
|
||||
print(" cp ~/gentoo/v4/hypr/ENVariables.conf ~/.config/hypr/UserConfigs/")
|
||||
print(" cp ~/gentoo/v4/hypr/monitors.conf ~/.config/hypr/")
|
||||
|
||||
|
||||
def cmd_all() -> None:
|
||||
"""Full installation workflow."""
|
||||
info("=== Gentoo Full Installation ===")
|
||||
print()
|
||||
|
||||
# Step 1: Disk preparation
|
||||
info("Step 1: Disk Preparation")
|
||||
layout, config = cmd_disk()
|
||||
|
||||
# Step 2: Stage3
|
||||
print()
|
||||
info("Step 2: Stage3 Download & Extract")
|
||||
cmd_stage3()
|
||||
|
||||
# Step 3: Configuration
|
||||
print()
|
||||
info("Step 3: Copy Configuration")
|
||||
cmd_config()
|
||||
|
||||
# Step 4: Generate fstab/crypttab
|
||||
print()
|
||||
info("Step 4: Generate Filesystem Config")
|
||||
cmd_fstab(layout, config)
|
||||
|
||||
# Step 5: Chroot
|
||||
print()
|
||||
info("Step 5: Chroot Preparation")
|
||||
cmd_chroot()
|
||||
|
||||
|
||||
def interactive_menu() -> None:
|
||||
"""Interactive command selection."""
|
||||
print()
|
||||
info("=== Gentoo Installer for Legion S7 15ACH6 ===")
|
||||
print()
|
||||
print("Commands (pre-chroot):")
|
||||
print(" 1) disk - Partition, encrypt, and mount disks")
|
||||
print(" 2) stage3 - Download and extract stage3")
|
||||
print(" 3) config - Copy portage configuration")
|
||||
print(" 4) fstab - Generate fstab/crypttab")
|
||||
print(" 5) chroot - Prepare and enter chroot")
|
||||
print(" 6) all - Full pre-chroot installation")
|
||||
print()
|
||||
print("Commands (inside chroot):")
|
||||
print(" 7) sync - Portage sync, profile, overlays (activates swap)")
|
||||
print(" 8) world - emerge -avuDN @world")
|
||||
print(" 9) firmware - Install linux-firmware")
|
||||
print(" 10) kernel - Install kernel")
|
||||
print(" 11) services - Install and configure services")
|
||||
print(" 12) users - Configure shell, sudo, create user")
|
||||
print(" 13) nvidia - Install NVIDIA drivers")
|
||||
print(" 14) bootloader - Install GRUB, verify initramfs")
|
||||
print()
|
||||
print("Post-install:")
|
||||
print(" 15) fingerprint - Set up fingerprint authentication")
|
||||
print()
|
||||
print("Utilities:")
|
||||
print(" 16) swap-on - Activate encrypted swap")
|
||||
print(" 17) swap-off - Deactivate encrypted swap")
|
||||
print()
|
||||
print("Full workflows:")
|
||||
print(" i) install - Full install to first reboot (auto-detects chroot)")
|
||||
print(" d) desktop - Post-reboot desktop setup (Hyprland + fingerprint)")
|
||||
print()
|
||||
print(" q) quit")
|
||||
print()
|
||||
|
||||
choice = prompt("Select command: ").strip().lower()
|
||||
|
||||
if choice in ("1", "disk"):
|
||||
cmd_disk()
|
||||
elif choice in ("2", "stage3"):
|
||||
cmd_stage3()
|
||||
elif choice in ("3", "config"):
|
||||
cmd_config()
|
||||
elif choice in ("4", "fstab"):
|
||||
if _disk_layout is None or _disk_config is None:
|
||||
error("Disk layout not available.")
|
||||
error("Run 'disk' command first in this session, or use 'all' for full installation.")
|
||||
return
|
||||
cmd_fstab(_disk_layout, _disk_config)
|
||||
elif choice in ("5", "chroot"):
|
||||
cmd_chroot()
|
||||
elif choice in ("6", "all"):
|
||||
cmd_all()
|
||||
elif choice in ("7", "sync"):
|
||||
cmd_sync()
|
||||
elif choice in ("8", "world"):
|
||||
cmd_world()
|
||||
elif choice in ("9", "firmware"):
|
||||
cmd_firmware()
|
||||
elif choice in ("10", "kernel"):
|
||||
cmd_kernel()
|
||||
elif choice in ("11", "services"):
|
||||
cmd_services()
|
||||
elif choice in ("12", "users"):
|
||||
cmd_users()
|
||||
elif choice in ("13", "nvidia"):
|
||||
cmd_nvidia()
|
||||
elif choice in ("14", "bootloader"):
|
||||
cmd_bootloader()
|
||||
elif choice in ("15", "fingerprint"):
|
||||
cmd_fingerprint()
|
||||
elif choice in ("16", "swap-on"):
|
||||
cmd_swap_on()
|
||||
elif choice in ("17", "swap-off"):
|
||||
cmd_swap_off()
|
||||
elif choice in ("i", "install"):
|
||||
cmd_install()
|
||||
elif choice in ("d", "desktop"):
|
||||
cmd_desktop()
|
||||
elif choice == "q":
|
||||
sys.exit(0)
|
||||
else:
|
||||
error("Invalid choice")
|
||||
interactive_menu()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Gentoo installation orchestrator",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__,
|
||||
)
|
||||
parser.add_argument(
|
||||
"command",
|
||||
nargs="?",
|
||||
choices=["disk", "stage3", "chroot", "config", "fstab", "sync", "world", "firmware", "kernel", "services", "users", "nvidia", "bootloader", "fingerprint", "swap-on", "swap-off", "all"],
|
||||
help="Command to run (interactive menu if not specified)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--install",
|
||||
action="store_true",
|
||||
help="Full installation up to first reboot (auto-detects chroot state)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--desktop",
|
||||
action="store_true",
|
||||
help="Post-reboot desktop setup (Hyprland + fingerprint)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
check_root()
|
||||
|
||||
# Handle major phase flags first
|
||||
if args.install:
|
||||
cmd_install()
|
||||
elif args.desktop:
|
||||
cmd_desktop()
|
||||
elif args.command is None:
|
||||
interactive_menu()
|
||||
elif args.command == "disk":
|
||||
cmd_disk()
|
||||
elif args.command == "stage3":
|
||||
cmd_stage3()
|
||||
elif args.command == "config":
|
||||
cmd_config()
|
||||
elif args.command == "fstab":
|
||||
# For standalone fstab, we need disk info
|
||||
error("Run 'disk' first, or use 'all' for full installation")
|
||||
sys.exit(1)
|
||||
elif args.command == "chroot":
|
||||
cmd_chroot()
|
||||
elif args.command == "sync":
|
||||
cmd_sync()
|
||||
elif args.command == "world":
|
||||
cmd_world()
|
||||
elif args.command == "firmware":
|
||||
cmd_firmware()
|
||||
elif args.command == "kernel":
|
||||
cmd_kernel()
|
||||
elif args.command == "services":
|
||||
cmd_services()
|
||||
elif args.command == "users":
|
||||
cmd_users()
|
||||
elif args.command == "nvidia":
|
||||
cmd_nvidia()
|
||||
elif args.command == "bootloader":
|
||||
cmd_bootloader()
|
||||
elif args.command == "fingerprint":
|
||||
cmd_fingerprint()
|
||||
elif args.command == "swap-on":
|
||||
cmd_swap_on()
|
||||
elif args.command == "swap-off":
|
||||
cmd_swap_off()
|
||||
elif args.command == "all":
|
||||
cmd_all()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
146
starship.toml
Normal file
146
starship.toml
Normal file
@ -0,0 +1,146 @@
|
||||
## FIRST LINE/ROW: Info & Status
|
||||
# First param ─┌
|
||||
[username]
|
||||
format = " [╭─$user]($style)@"
|
||||
show_always = true
|
||||
style_root = "bold #ff5f5f"
|
||||
style_user = "bold #61afef"
|
||||
|
||||
# Second param
|
||||
[hostname]
|
||||
disabled = false
|
||||
format = "[$hostname]($style) in "
|
||||
ssh_only = false
|
||||
style = "bold #56b6c2"
|
||||
trim_at = "-"
|
||||
|
||||
# Third param
|
||||
[directory]
|
||||
style = "#c678dd"
|
||||
truncate_to_repo = true
|
||||
truncation_length = 0
|
||||
truncation_symbol = "repo: "
|
||||
|
||||
# Fourth param
|
||||
[sudo]
|
||||
disabled = false
|
||||
|
||||
# Before all the version info (python, nodejs, php, etc.)
|
||||
[git_status]
|
||||
ahead = "⇡${count}"
|
||||
behind = "⇣${count}"
|
||||
deleted = "x"
|
||||
diverged = "⇕⇡${ahead_count}⇣${behind_count}"
|
||||
style = "bold #e5c07b"
|
||||
|
||||
# Last param in the first line/row
|
||||
[cmd_duration]
|
||||
disabled = false
|
||||
format = "took [$duration]($style)"
|
||||
min_time = 1
|
||||
|
||||
## SECOND LINE/ROW: Prompt
|
||||
# Somethere at the beginning
|
||||
[battery]
|
||||
charging_symbol = ""
|
||||
disabled = true
|
||||
discharging_symbol = ""
|
||||
full_symbol = ""
|
||||
|
||||
[[battery.display]] # "bold red" style when capacity is between 0% and 15%
|
||||
disabled = false
|
||||
style = "bold #ff5f5f"
|
||||
threshold = 15
|
||||
|
||||
[[battery.display]] # "bold yellow" style when capacity is between 15% and 50%
|
||||
disabled = true
|
||||
style = "bold #e5c07b"
|
||||
threshold = 50
|
||||
|
||||
[[battery.display]] # "bold green" style when capacity is between 50% and 80%
|
||||
disabled = true
|
||||
style = "bold #98c379"
|
||||
threshold = 80
|
||||
|
||||
# Prompt: optional param 1
|
||||
[time]
|
||||
disabled = true
|
||||
format = " 🕙 $time($style)\n"
|
||||
style = "#abb2bf"
|
||||
time_format = "%T"
|
||||
|
||||
# Prompt: param 2
|
||||
[character]
|
||||
error_symbol = " [×](bold #ff5f5f)"
|
||||
success_symbol = " [╰─>](bold #61afef)"
|
||||
|
||||
# SYMBOLS
|
||||
[status]
|
||||
disabled = false
|
||||
format = '[\[$symbol$status_common_meaning$status_signal_name$status_maybe_int\]]($style)'
|
||||
map_symbol = true
|
||||
pipestatus = true
|
||||
symbol = "🔴"
|
||||
|
||||
[aws]
|
||||
symbol = " "
|
||||
|
||||
[conda]
|
||||
symbol = " "
|
||||
|
||||
[dart]
|
||||
symbol = " "
|
||||
|
||||
[docker_context]
|
||||
symbol = " "
|
||||
|
||||
[elixir]
|
||||
symbol = " "
|
||||
|
||||
[elm]
|
||||
symbol = " "
|
||||
|
||||
[git_branch]
|
||||
symbol = " "
|
||||
|
||||
[golang]
|
||||
symbol = " "
|
||||
|
||||
[hg_branch]
|
||||
symbol = " "
|
||||
|
||||
[java]
|
||||
symbol = " "
|
||||
|
||||
[julia]
|
||||
symbol = " "
|
||||
|
||||
[nim]
|
||||
symbol = " "
|
||||
|
||||
[nix_shell]
|
||||
symbol = " "
|
||||
|
||||
[nodejs]
|
||||
symbol = " "
|
||||
|
||||
[package]
|
||||
symbol = " "
|
||||
|
||||
[perl]
|
||||
symbol = " "
|
||||
|
||||
[php]
|
||||
symbol = " "
|
||||
|
||||
[python]
|
||||
symbol = " "
|
||||
|
||||
[ruby]
|
||||
symbol = " "
|
||||
|
||||
[rust]
|
||||
symbol = " "
|
||||
|
||||
[swift]
|
||||
symbol = " "
|
||||
3
udev/99-power-profile.rules
Normal file
3
udev/99-power-profile.rules
Normal file
@ -0,0 +1,3 @@
|
||||
# Automatic power profile switching based on AC state
|
||||
ACTION=="change", SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="1", RUN+="/usr/bin/powerprofilesctl set performance"
|
||||
ACTION=="change", SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="0", RUN+="/usr/bin/powerprofilesctl set power-saver"
|
||||
Loading…
x
Reference in New Issue
Block a user