public-ready-init

This commit is contained in:
Damien Coles 2026-02-05 18:05:06 -05:00
commit 6e8d1c9392
61 changed files with 4672 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
__pycache__/
*.pyc
*.pyo
.DS_Store
*.swp
*.swo
*~

10
.idea/.gitignore generated vendored Normal file
View 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
View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,2 @@
# LUKS support for encrypted root
add_dracutmodules+=" crypt "

View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
"""Gentoo installation automation for Legion S7 15ACH6."""
__version__ = "4.0.0"

227
install/bootloader.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
# Reduced parallelism for memory-hungry builds
MAKEOPTS="-j6"
NINJAOPTS="-j6"

55
portage/make.conf Normal file
View 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"

View File

@ -0,0 +1,2 @@
# GVFS virtual filesystem
gnome-base/gvfs ~amd64

View 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

View File

@ -0,0 +1,2 @@
# JetBrains Toolbox
dev-util/jetbrains-toolbox ~amd64

View File

@ -0,0 +1,3 @@
# NVIDIA drivers and EGL Wayland
x11-drivers/nvidia-drivers ~amd64
gui-libs/egl-wayland2 ~amd64

View File

@ -0,0 +1,3 @@
# Podman container tools
app-containers/podman-compose ~amd64
app-containers/podman-tui ~amd64

View File

@ -0,0 +1,4 @@
# Steam overlay
*/*::steam-overlay
games-util/game-device-udev-rules
sys-libs/libudev-compat

View File

@ -0,0 +1,3 @@
# UDisks disk management
sys-fs/udisks ~amd64
sys-libs/libblockdev ~amd64

View File

@ -0,0 +1 @@
llvm-core/clang reduced

View File

@ -0,0 +1 @@
gui-wm/hyprland clang

1
portage/package.env/llvm Normal file
View File

@ -0,0 +1 @@
llvm-core/llvm reduced

View File

@ -0,0 +1,2 @@
# webkit-gtk is extremely memory-hungry
net-libs/webkit-gtk reduced

View 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
View File

@ -0,0 +1,2 @@
# GVFS - FUSE support for user-space mounts
gnome-base/gvfs fuse

View 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

View File

@ -0,0 +1,2 @@
# Required by podman for container networking
net-firewall/iptables nftables

4
portage/package.use/llvm Normal file
View 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

View File

@ -0,0 +1,2 @@
# Node.js - include npm package manager
net-libs/nodejs npm

View File

@ -0,0 +1,2 @@
# Podman container runtime
app-containers/podman wrapper

140
portage/package.use/steam Normal file
View 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

View File

@ -0,0 +1,2 @@
# Waybar - status bar for Wayland
gui-apps/waybar network tray mpris

173
portage/sets/hyprland Normal file
View 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
View 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
View 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
View 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 = " "

View 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"