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