Merge master into colored-install-preview

This commit is contained in:
Softer 2026-06-03 16:22:40 +03:00
commit 6e9bdc36c2
96 changed files with 5001 additions and 2841 deletions

View File

@ -32,7 +32,7 @@ jobs:
- run: cat /etc/os-release
- run: pacman-key --init
- run: pacman --noconfirm -Sy archlinux-keyring
- run: ./build_iso.sh
- run: ./test_tooling/mkarchiso/build_iso.sh
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: Arch Live ISO

View File

@ -0,0 +1,22 @@
name: Translation validation
on:
push:
paths:
- 'archinstall/**/*.py'
- 'archinstall/locales/**'
- '.github/workflows/translation-check.yaml'
pull_request:
paths:
- 'archinstall/**/*.py'
- 'archinstall/locales/**'
- '.github/workflows/translation-check.yaml'
jobs:
translations:
name: Validate translations
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install gettext
run: sudo apt-get update && sudo apt-get install -y gettext
- name: Run translation checks
run: bash archinstall/locales/locales_generator.sh check

40
.github/workflows/uki-build.yaml vendored Normal file
View File

@ -0,0 +1,40 @@
# This workflow will build an Arch Linux UKI file with the commit on it
name: Build Arch UKI with ArchInstall Commit
on:
push:
branches:
- master
- main # In case we adopt this convention in the future
pull_request:
paths-ignore:
- 'docs/**'
- '**.editorconfig'
- '**.gitignore'
- '**.md'
- 'LICENSE'
- 'PKGBUILD'
release:
types:
- created
jobs:
build:
runs-on: ubuntu-latest
container:
image: archlinux/archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- run: pwd
- run: find .
- run: cat /etc/os-release
- run: pacman-key --init
- run: pacman --noconfirm -Sy archlinux-keyring
- run: pacman --noconfirm -Sy mkosi
- run: (cd test_tooling/mkosi/ && mkosi build -B)
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: Arch Live UKI
path: test_tooling/mkosi/mkosi.output/*.efi

4
.gitignore vendored
View File

@ -41,3 +41,7 @@ requirements.txt
/cmd_output.txt
node_modules/
uv.lock
test_tooling/mkosi/mkosi.output/*image*
test_tooling/mkosi/mkosi.cache/**
test_tooling/mkosi/mkosi.tools/**
test_tooling/mkosi/mkosi.tools.manifest

View File

@ -1,7 +1,7 @@
default_stages: ['pre-commit']
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.12
rev: v0.15.14
hooks:
# fix unused imports and sort them
- id: ruff
@ -41,6 +41,7 @@ repos:
additional_dependencies:
- pydantic
- pytest
- hypothesis
- cryptography
- textual
- repo: local

View File

@ -1,9 +1,9 @@
from typing import TYPE_CHECKING
from archinstall.lib.hardware import SysInfo
from archinstall.lib.log import debug
from archinstall.lib.models.application import Audio, AudioConfiguration
from archinstall.lib.models.users import User
from archinstall.lib.output import debug
if TYPE_CHECKING:
from archinstall.lib.installer import Installer

View File

@ -1,6 +1,6 @@
from typing import TYPE_CHECKING
from archinstall.lib.output import debug
from archinstall.lib.log import debug
if TYPE_CHECKING:
from archinstall.lib.installer import Installer

View File

@ -1,7 +1,7 @@
from typing import TYPE_CHECKING
from archinstall.lib.log import debug
from archinstall.lib.models.application import Firewall, FirewallConfiguration
from archinstall.lib.output import debug
if TYPE_CHECKING:
from archinstall.lib.installer import Installer

View File

@ -1,7 +1,7 @@
from typing import TYPE_CHECKING
from archinstall.lib.log import debug
from archinstall.lib.models.application import FontsConfiguration
from archinstall.lib.output import debug
if TYPE_CHECKING:
from archinstall.lib.installer import Installer

View File

@ -1,7 +1,7 @@
from typing import TYPE_CHECKING
from archinstall.lib.log import debug
from archinstall.lib.models.application import PowerManagement, PowerManagementConfiguration
from archinstall.lib.output import debug
if TYPE_CHECKING:
from archinstall.lib.installer import Installer

View File

@ -1,6 +1,6 @@
from typing import TYPE_CHECKING
from archinstall.lib.output import debug
from archinstall.lib.log import debug
if TYPE_CHECKING:
from archinstall.lib.installer import Installer

View File

@ -1,8 +1,8 @@
from typing import TYPE_CHECKING, Self, override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType, SelectResult
from archinstall.lib.log import info
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.output import info
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType

View File

@ -7,7 +7,7 @@ from archinstall.default_profiles.profile import CustomSetting, DisplayServerTyp
class NiriProfile(Profile):
def __init__(self) -> None:
super().__init__(
'Niri',
'niri',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,

View File

@ -0,0 +1,66 @@
import shutil
from pathlib import Path
from typing import TYPE_CHECKING, override
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
if TYPE_CHECKING:
from archinstall.lib.installer import Installer
from archinstall.lib.models.users import User
_TERMINAL = 'alacritty'
_ASSETS_DIR = Path(__file__).parent / 'niri_dms_assets'
class NiriDmsProfile(Profile):
def __init__(self) -> None:
super().__init__(
'niri - DankMaterialShell',
ProfileType.WindowMgr,
support_gfx_driver=True,
display_server=DisplayServerType.Wayland,
)
@property
@override
def packages(self) -> list[str]:
return [
'niri',
'dms-shell-niri',
'polkit',
'xdg-desktop-portal-gnome',
'xorg-xwayland',
'matugen',
'cava',
'kimageformats',
'cups-pk-helper',
'tuned-ppd',
_TERMINAL,
]
@property
@override
def default_greeter_type(self) -> GreeterType:
return GreeterType.GreetdDms
@override
def provision(self, install_session: Installer, users: list[User]) -> None:
binds = (_ASSETS_DIR / 'dms/binds.kdl').read_text().replace('{{TERMINAL_COMMAND}}', _TERMINAL)
for user in users:
home = install_session.target / 'home' / user.username
niri_dir = home / '.config/niri'
dms_dir = niri_dir / 'dms'
dms_dir.mkdir(parents=True, exist_ok=True)
shutil.copy(_ASSETS_DIR / 'niri.kdl', niri_dir / 'config.kdl')
for name in ('colors.kdl', 'layout.kdl', 'alttab.kdl', 'outputs.kdl', 'cursor.kdl'):
shutil.copy(_ASSETS_DIR / 'dms' / name, dms_dir / name)
(dms_dir / 'binds.kdl').write_text(binds)
niri_unit_dropin = home / '.config/systemd/user/niri.service.d'
niri_unit_dropin.mkdir(parents=True, exist_ok=True)
(niri_unit_dropin / 'dms.conf').write_text('[Unit]\nWants=dms.service\n')
install_session.arch_chroot(f'chown -R {user.username}:{user.username} /home/{user.username}/.config')

View File

@ -0,0 +1,10 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
recent-windows {
highlight {
corner-radius 12
}
}

View File

@ -0,0 +1,221 @@
binds {
// === System & Overview ===
Mod+D repeat=false { toggle-overview; }
Mod+Tab repeat=false { toggle-overview; }
Mod+Shift+Slash { show-hotkey-overlay; }
// === Application Launchers ===
Mod+T hotkey-overlay-title="Open Terminal" { spawn "{{TERMINAL_COMMAND}}"; }
Mod+Space hotkey-overlay-title="Application Launcher" {
spawn "dms" "ipc" "call" "spotlight" "toggle";
}
Mod+V hotkey-overlay-title="Clipboard Manager" {
spawn "dms" "ipc" "call" "clipboard" "toggle";
}
Mod+M hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
}
Super+X hotkey-overlay-title="Power Menu: Toggle" { spawn "dms" "ipc" "call" "powermenu" "toggle"; }
Mod+Comma hotkey-overlay-title="Settings" {
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
}
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
}
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
// === Security ===
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
spawn "dms" "ipc" "call" "lock" "lock";
}
Mod+Shift+E { quit; }
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
}
// === Audio Controls ===
XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "increment" "3";
}
XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "decrement" "3";
}
XF86AudioMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "mute";
}
XF86AudioMicMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "micmute";
}
XF86AudioPause allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "playPause";
}
XF86AudioPlay allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "playPause";
}
XF86AudioPrev allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "previous";
}
XF86AudioNext allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "next";
}
Ctrl+XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "increment" "3";
}
Ctrl+XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "mpris" "decrement" "3";
}
// === Brightness Controls ===
XF86MonBrightnessUp allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
}
XF86MonBrightnessDown allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
}
// === Window Management ===
Mod+Q repeat=false { close-window; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+Shift+T { toggle-window-floating; }
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
Mod+W { toggle-column-tabbed-display; }
Mod+Shift+W hotkey-overlay-title="Create window rule" { spawn "dms" "ipc" "call" "window-rules" "toggle"; }
// === Focus Navigation ===
Mod+Left { focus-column-left; }
Mod+Down { focus-window-down; }
Mod+Up { focus-window-up; }
Mod+Right { focus-column-right; }
Mod+H { focus-column-left; }
Mod+J { focus-window-down; }
Mod+K { focus-window-up; }
Mod+L { focus-column-right; }
// === Window Movement ===
Mod+Shift+Left { move-column-left; }
Mod+Shift+Down { move-window-down; }
Mod+Shift+Up { move-window-up; }
Mod+Shift+Right { move-column-right; }
Mod+Shift+H { move-column-left; }
Mod+Shift+J { move-window-down; }
Mod+Shift+K { move-window-up; }
Mod+Shift+L { move-column-right; }
// === Column Navigation ===
Mod+Home { focus-column-first; }
Mod+End { focus-column-last; }
Mod+Ctrl+Home { move-column-to-first; }
Mod+Ctrl+End { move-column-to-last; }
// === Monitor Navigation ===
Mod+Ctrl+Left { focus-monitor-left; }
//Mod+Ctrl+Down { focus-monitor-down; }
//Mod+Ctrl+Up { focus-monitor-up; }
Mod+Ctrl+Right { focus-monitor-right; }
Mod+Ctrl+H { focus-monitor-left; }
Mod+Ctrl+J { focus-monitor-down; }
Mod+Ctrl+K { focus-monitor-up; }
Mod+Ctrl+L { focus-monitor-right; }
// === Move to Monitor ===
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
// === Workspace Navigation ===
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Ctrl+Down { move-column-to-workspace-down; }
Mod+Ctrl+Up { move-column-to-workspace-up; }
Mod+Ctrl+U { move-column-to-workspace-down; }
Mod+Ctrl+I { move-column-to-workspace-up; }
// === Workspace Management ===
Ctrl+Shift+R hotkey-overlay-title="Rename Workspace" {
spawn "dms" "ipc" "call" "workspace-rename" "open";
}
// === Move Workspaces ===
Mod+Shift+Page_Down { move-workspace-down; }
Mod+Shift+Page_Up { move-workspace-up; }
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
// === Mouse Wheel Navigation ===
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
Mod+WheelScrollRight { focus-column-right; }
Mod+WheelScrollLeft { focus-column-left; }
Mod+Ctrl+WheelScrollRight { move-column-right; }
Mod+Ctrl+WheelScrollLeft { move-column-left; }
Mod+Shift+WheelScrollDown { focus-column-right; }
Mod+Shift+WheelScrollUp { focus-column-left; }
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
// === Numbered Workspaces ===
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
Mod+4 { focus-workspace 4; }
Mod+5 { focus-workspace 5; }
Mod+6 { focus-workspace 6; }
Mod+7 { focus-workspace 7; }
Mod+8 { focus-workspace 8; }
Mod+9 { focus-workspace 9; }
// === Move to Numbered Workspaces ===
Mod+Shift+1 { move-column-to-workspace 1; }
Mod+Shift+2 { move-column-to-workspace 2; }
Mod+Shift+3 { move-column-to-workspace 3; }
Mod+Shift+4 { move-column-to-workspace 4; }
Mod+Shift+5 { move-column-to-workspace 5; }
Mod+Shift+6 { move-column-to-workspace 6; }
Mod+Shift+7 { move-column-to-workspace 7; }
Mod+Shift+8 { move-column-to-workspace 8; }
Mod+Shift+9 { move-column-to-workspace 9; }
// === Column Management ===
Mod+BracketLeft { consume-or-expel-window-left; }
Mod+BracketRight { consume-or-expel-window-right; }
Mod+Period { expel-window-from-column; }
// === Sizing & Layout ===
Mod+R { switch-preset-column-width; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+Ctrl+F { expand-column-to-available-width; }
Mod+C { center-column; }
Mod+Ctrl+C { center-visible-columns; }
// === Manual Sizing ===
Mod+Minus { set-column-width "-10%"; }
Mod+Equal { set-column-width "+10%"; }
Mod+Shift+Minus { set-window-height "-10%"; }
Mod+Shift+Equal { set-window-height "+10%"; }
// === Screenshots ===
XF86Launch1 { screenshot; }
Ctrl+XF86Launch1 { screenshot-screen; }
Alt+XF86Launch1 { screenshot-window; }
Print { screenshot; }
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// === System Controls ===
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
Mod+Shift+P { power-off-monitors; }
}

View File

@ -0,0 +1,39 @@
// ! Auto-generated file. Do not edit directly.
// Remove `include "dms/colors.kdl"` from your config to override.
layout {
background-color "transparent"
focus-ring {
active-color "#d0bcff"
inactive-color "#948f99"
urgent-color "#f2b8b5"
}
border {
active-color "#d0bcff"
inactive-color "#948f99"
urgent-color "#f2b8b5"
}
shadow {
color "#00000070"
}
tab-indicator {
active-color "#d0bcff"
inactive-color "#948f99"
urgent-color "#f2b8b5"
}
insert-hint {
color "#d0bcff80"
}
}
recent-windows {
highlight {
active-color "#4f378b"
urgent-color "#f2b8b5"
}
}

View File

@ -0,0 +1,6 @@
// Place cursor configuration here.
// Example:
// cursor {
// xcursor-theme "Adwaita"
// xcursor-size 24
// }

View File

@ -0,0 +1,22 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
layout {
gaps 4
border {
width 2
}
focus-ring {
width 2
}
}
window-rule {
geometry-corner-radius 12
clip-to-geometry true
tiled-state true
draw-border-with-background false
}

View File

@ -0,0 +1,7 @@
// Place per-output configuration here.
// Example:
// output "DP-1" {
// mode "2560x1440@165"
// position x=0 y=0
// scale 1
// }

View File

@ -0,0 +1,279 @@
// This config is in the KDL format: https://kdl.dev
// "/-" comments out the following node.
// Check the wiki for a full description of the configuration:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Introduction
config-notification {
disable-failed
}
gestures {
hot-corners {
off
}
}
// Input device configuration.
// Find the full list of options on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Input
input {
keyboard {
xkb {
// You can set rules, model, layout, variant and options.
// For more information, see xkeyboard-config(7).
// For example:
// layout "us,ru"
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
// If this section is empty, niri will fetch xkb settings
// from org.freedesktop.locale1. You can control these using
// localectl set-x11-keymap.
}
// Enable numlock on startup, omitting this setting disables it.
numlock
}
// Next sections include libinput settings.
// Omitting settings disables them, or leaves them at their default values.
// All commented-out settings here are examples, not defaults.
touchpad {
// off
tap
// dwt
// dwtp
// drag false
// drag-lock
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "two-finger"
// disabled-on-external-mouse
}
mouse {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "no-scroll"
}
trackpoint {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "on-button-down"
// scroll-button 273
// scroll-button-lock
// middle-emulation
}
// Uncomment this to make the mouse warp to the center of newly focused windows.
// warp-mouse-to-focus
// Focus windows and outputs automatically when moving the mouse into them.
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
// focus-follows-mouse max-scroll-amount="0%"
}
// You can configure outputs by their name, which you can find
// by running `niri msg outputs` while inside a niri instance.
// The built-in laptop monitor is usually called "eDP-1".
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs
// Remember to uncomment the node by removing "/-"!
/-output "eDP-2" {
mode "2560x1600@239.998993"
position x=2560 y=0
variable-refresh-rate
}
// Settings that influence how windows are positioned and sized.
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
layout {
// Set gaps around windows in logical pixels.
background-color "transparent"
// When to center a column when changing focus, options are:
// - "never", default behavior, focusing an off-screen column will keep at the left
// or right edge of the screen.
// - "always", the focused column will always be centered.
// - "on-overflow", focusing a column will center it if it doesn't fit
// together with the previously focused column.
center-focused-column "never"
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
preset-column-widths {
// Proportion sets the width as a fraction of the output width, taking gaps into account.
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
proportion 0.33333
proportion 0.5
proportion 0.66667
// Fixed sets the width in logical pixels exactly.
// fixed 1920
}
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
// preset-window-heights { }
// You can change the default width of the new windows.
default-column-width { proportion 0.5; }
// If you leave the brackets empty, the windows themselves will decide their initial width.
// default-column-width {}
// By default focus ring and border are rendered as a solid background rectangle
// behind windows. That is, they will show up through semitransparent windows.
// This is because windows using client-side decorations can have an arbitrary shape.
//
// If you don't like that, you should uncomment `prefer-no-csd` below.
// Niri will draw focus ring and border *around* windows that agree to omit their
// client-side decorations.
//
// Alternatively, you can override it with a window rule called
// `draw-border-with-background`.
border {
off
width 4
active-color "#707070" // Neutral gray
inactive-color "#d0d0d0" // Light gray
urgent-color "#cc4444" // Softer red
}
shadow {
softness 30
spread 5
offset x=0 y=5
color "#0007"
}
struts {
}
}
layer-rule {
match namespace="^quickshell$"
place-within-backdrop true
}
overview {
workspace-shadow {
off
}
}
// Add lines like this to spawn processes at startup.
// Note that running niri as a session supports xdg-desktop-autostart,
// which may be more convenient to use.
// See the binds section below for more spawn examples.
// This line starts waybar, a commonly used bar for Wayland compositors.
environment {
XDG_CURRENT_DESKTOP "niri"
}
hotkey-overlay {
skip-at-startup
}
prefer-no-csd
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
animations {
workspace-switch {
spring damping-ratio=0.80 stiffness=523 epsilon=0.0001
}
window-open {
duration-ms 150
curve "ease-out-expo"
}
window-close {
duration-ms 150
curve "ease-out-quad"
}
horizontal-view-movement {
spring damping-ratio=0.85 stiffness=423 epsilon=0.0001
}
window-movement {
spring damping-ratio=0.75 stiffness=323 epsilon=0.0001
}
window-resize {
spring damping-ratio=0.85 stiffness=423 epsilon=0.0001
}
config-notification-open-close {
spring damping-ratio=0.65 stiffness=923 epsilon=0.001
}
screenshot-ui-open {
duration-ms 200
curve "ease-out-quad"
}
overview-open-close {
spring damping-ratio=0.85 stiffness=800 epsilon=0.0001
}
}
// Window rules let you adjust behavior for individual windows.
// Find more information on the wiki:
// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules
// Work around WezTerm's initial configure bug
// by setting an empty default-column-width.
window-rule {
// This regular expression is intentionally made as specific as possible,
// since this is the default config, and we want no false positives.
// You can get away with just app-id="wezterm" if you want.
match app-id=r#"^org\.wezfurlong\.wezterm$"#
default-column-width {}
}
window-rule {
match app-id=r#"^org\.gnome\."#
draw-border-with-background false
geometry-corner-radius 12
clip-to-geometry true
}
window-rule {
match app-id=r#"^gnome-control-center$"#
match app-id=r#"^pavucontrol$"#
match app-id=r#"^nm-connection-editor$"#
default-column-width { proportion 0.5; }
open-floating false
}
window-rule {
match app-id=r#"^org\.gnome\.Calculator$"#
match app-id=r#"^gnome-calculator$"#
match app-id=r#"^galculator$"#
match app-id=r#"^blueman-manager$"#
match app-id=r#"^org\.gnome\.Nautilus$"#
match app-id=r#"^xdg-desktop-portal$"#
open-floating true
}
window-rule {
match app-id=r#"^steam$"# title=r#"^notificationtoasts_\d+_desktop$"#
default-floating-position x=10 y=10 relative-to="bottom-right"
open-focused false
}
window-rule {
match app-id=r#"^org\.wezfurlong\.wezterm$"#
match app-id="Alacritty"
match app-id="zen"
match app-id="com.mitchellh.ghostty"
match app-id="kitty"
draw-border-with-background false
}
window-rule {
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
match app-id="zoom"
open-floating true
}
// Open dms windows as floating by default
window-rule {
match app-id=r#"org.quickshell$"#
match app-id=r#"com.danklinux.dms$"#
open-floating true
}
debug {
honor-xdg-activation-with-invalid-serial
}
// Override to disable super+tab
recent-windows {
binds {
Alt+Tab { next-window scope="output"; }
Alt+Shift+Tab { previous-window scope="output"; }
Alt+grave { next-window filter="app-id"; }
Alt+Shift+grave { previous-window filter="app-id"; }
}
}
// Include dms files
include "dms/colors.kdl"
include "dms/layout.kdl"
include "dms/alttab.kdl"
include "dms/binds.kdl"
include "dms/outputs.kdl"
include "dms/cursor.kdl"

View File

@ -37,6 +37,7 @@ class GreeterType(Enum):
Ly = 'ly'
CosmicSession = 'cosmic-greeter'
PlasmaLoginManager = 'plasma-login-manager'
GreetdDms = 'dms-greeter'
class SelectResult(Enum):

View File

@ -1,8 +1,8 @@
from typing import TYPE_CHECKING, Self, override
from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult
from archinstall.lib.log import info
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.output import info
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType

View File

@ -6,7 +6,7 @@ import urllib.error
import urllib.parse
from argparse import ArgumentParser, Namespace
from dataclasses import dataclass, field
from enum import StrEnum
from enum import Enum, StrEnum
from pathlib import Path
from typing import Any, Self
from urllib.request import Request, urlopen
@ -14,6 +14,7 @@ from urllib.request import Request, urlopen
from pydantic.dataclasses import dataclass as p_dataclass
from archinstall.lib.crypt import decrypt
from archinstall.lib.log import debug, error, logger, warn
from archinstall.lib.menu.util import get_password
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
from archinstall.lib.models.authentication import AuthenticationConfiguration
@ -28,13 +29,16 @@ from archinstall.lib.models.packages import Repository
from archinstall.lib.models.pacman import PacmanConfiguration
from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.models.users import Password, User, UserSerialization
from archinstall.lib.output import debug, error, logger, warn
from archinstall.lib.plugins import load_plugin
from archinstall.lib.translationhandler import Language, tr, translation_handler
from archinstall.lib.version import get_version
from archinstall.tui.components import tui
class SubCommand(Enum):
SHARE_LOG = 'share-log'
@p_dataclass
class Arguments:
config: Path | None = None
@ -58,6 +62,8 @@ class Arguments:
advanced: bool = False
verbose: bool = False
command: SubCommand | None = None
class ArchConfigType(StrEnum):
VERSION = 'version'
@ -365,13 +371,13 @@ class ArchConfig:
class ArchConfigHandler:
def __init__(self) -> None:
self._parser: ArgumentParser = self._define_arguments()
args: Arguments = self._parse_args()
self._args = args
self._add_sub_parsers()
self._args: Arguments = self._parse_args()
config = self._parse_config()
try:
self._config = ArchConfig.from_config(config, args)
self._config = ArchConfig.from_config(config, self._args)
self._config.version = get_version()
except ValueError as err:
warn(str(err))
@ -397,8 +403,13 @@ class ArchConfigHandler:
def print_help(self) -> None:
self._parser.print_help()
def _add_sub_parsers(self) -> None:
subparsers = self._parser.add_subparsers(dest='command', help='Available subcommands')
_ = subparsers.add_parser(SubCommand.SHARE_LOG.value, help='Upload log file to public server')
def _define_arguments(self) -> ArgumentParser:
parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(
'-v',
'--version',

View File

@ -3,9 +3,9 @@ from pathlib import Path
from typing import TYPE_CHECKING
from archinstall.lib.command import SysCommandWorker
from archinstall.lib.log import debug, info
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
from archinstall.lib.models.users import User
from archinstall.lib.output import debug, info
from archinstall.lib.translationhandler import tr
if TYPE_CHECKING:

View File

@ -6,9 +6,9 @@ from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.menu.util import get_password
from archinstall.lib.models.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
from archinstall.lib.models.users import Password, User
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.lib.user.user_menu import select_users
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
@ -65,7 +65,7 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
users: list[User] | None = item.value
if users:
return FormattedOutput.as_table(users)
return as_table(users)
return None
def _prev_root_pwd(self, item: MenuItem) -> str | None:

View File

@ -6,7 +6,7 @@ from typing import ClassVar, Self
from archinstall.lib.command import SysCommand, SysCommandWorker
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.output import error
from archinstall.lib.log import error
class Boot:

View File

@ -11,7 +11,7 @@ from types import TracebackType
from typing import Any, Self, override
from archinstall.lib.exceptions import RequirementError, SysCallError
from archinstall.lib.output import debug, error, logger
from archinstall.lib.log import debug, error, logger
from archinstall.lib.utils.encoding import clear_vt100_escape_codes

View File

@ -8,11 +8,12 @@ from pydantic import TypeAdapter
from archinstall.lib.args import ArchConfig, ArchConfigType
from archinstall.lib.crypt import encrypt
from archinstall.lib.log import debug, logger, warn
from archinstall.lib.menu.helpers import Confirmation, Selection
from archinstall.lib.menu.util import get_password, prompt_dir
from archinstall.lib.models.network import NetworkConfiguration
from archinstall.lib.output import as_key_value_pair, debug, logger, warn
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_key_value_pair
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType

View File

@ -6,7 +6,7 @@ from pathlib import Path
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
from archinstall.lib.output import debug
from archinstall.lib.log import debug
libcrypt = ctypes.CDLL('libcrypt.so')

View File

@ -15,6 +15,7 @@ from archinstall.lib.disk.utils import (
umount,
)
from archinstall.lib.exceptions import DiskError, SysCallError, UnknownFilesystemFormat
from archinstall.lib.log import debug, error, info, log
from archinstall.lib.models.device import (
DEFAULT_ITER_TIME,
BDevice,
@ -35,7 +36,6 @@ from archinstall.lib.models.device import (
_PartitionInfo,
)
from archinstall.lib.models.users import Password
from archinstall.lib.output import debug, error, info, log
from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT

View File

@ -5,6 +5,7 @@ from typing import override
from archinstall.lib.disk.device_handler import device_handler
from archinstall.lib.disk.encryption_menu import DiskEncryptionMenu
from archinstall.lib.disk.partitioning_menu import manual_partitioning
from archinstall.lib.log import debug
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
from archinstall.lib.menu.helpers import Confirmation, Notify, Selection, Table
from archinstall.lib.menu.util import prompt_dir
@ -35,8 +36,8 @@ from archinstall.lib.models.device import (
Unit,
_DeviceInfo,
)
from archinstall.lib.output import FormattedOutput, debug
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
@ -221,7 +222,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
for mod in device_mods:
# create partition table
partition_table = FormattedOutput.as_table(mod.partitions)
partition_table = as_table(mod.partitions)
output_partition += f'{mod.device_path}: {mod.device.device_info.model}\n'
output_partition += '{}: {}\n'.format(tr('Wipe'), mod.wipe)
@ -230,7 +231,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
# create btrfs table
btrfs_partitions = [p for p in mod.partitions if p.btrfs_subvols]
for partition in btrfs_partitions:
output_btrfs += FormattedOutput.as_table(partition.btrfs_subvols) + '\n'
output_btrfs += as_table(partition.btrfs_subvols) + '\n'
output = output_partition + output_btrfs
return output.rstrip()
@ -246,12 +247,12 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
output = '{}: {}\n'.format(tr('Configuration'), lvm_config.config_type.display_msg())
for vol_gp in lvm_config.vol_groups:
pv_table = FormattedOutput.as_table(vol_gp.pvs)
pv_table = as_table(vol_gp.pvs)
output += '{}:\n{}'.format(tr('Physical volumes'), pv_table)
output += f'\nVolume Group: {vol_gp.name}'
lvm_volumes = FormattedOutput.as_table(vol_gp.volumes)
lvm_volumes = as_table(vol_gp.volumes)
output += '\n\n{}:\n{}'.format(tr('Volumes'), lvm_volumes)
return output
@ -302,7 +303,7 @@ async def select_devices(preset: list[BDevice] | None = []) -> list[BDevice] | N
dev = device_handler.get_device(device.path)
if dev and dev.partition_infos:
return FormattedOutput.as_table(dev.partition_infos)
return as_table(dev.partition_infos)
return None
if preset is None:

View File

@ -17,8 +17,8 @@ from archinstall.lib.models.device import (
PartitionModification,
)
from archinstall.lib.models.users import Password
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
@ -199,7 +199,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
def _prev_partitions(self, item: MenuItem) -> str | None:
if item.value:
output = tr('Partitions to be encrypted') + '\n'
output += FormattedOutput.as_table(item.value)
output += as_table(item.value)
return output.rstrip()
return None
@ -207,7 +207,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
def _prev_lvm_vols(self, item: MenuItem) -> str | None:
if item.value:
output = tr('LVM volumes to be encrypted') + '\n'
output += FormattedOutput.as_table(item.value)
output += as_table(item.value)
return output.rstrip()
return None

View File

@ -4,9 +4,9 @@ from typing import ClassVar
from archinstall.lib.command import SysCommand, SysCommandWorker
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import error, info
from archinstall.lib.models.device import Fido2Device
from archinstall.lib.models.users import Password
from archinstall.lib.output import error, info
from archinstall.lib.utils.encoding import clear_vt100_escape_codes_from_str

View File

@ -13,6 +13,7 @@ from archinstall.lib.disk.lvm import (
lvm_vol_reduce,
)
from archinstall.lib.disk.utils import udev_sync
from archinstall.lib.log import debug, info
from archinstall.lib.models.device import (
DiskEncryption,
DiskLayoutConfiguration,
@ -27,7 +28,6 @@ from archinstall.lib.models.device import (
Size,
Unit,
)
from archinstall.lib.output import debug, info
class FilesystemHandler:

View File

@ -7,9 +7,9 @@ from types import TracebackType
from archinstall.lib.command import SysCommand, SysCommandWorker, run
from archinstall.lib.disk.utils import get_lsblk_info, umount
from archinstall.lib.exceptions import DiskError, SysCallError
from archinstall.lib.log import debug, info
from archinstall.lib.models.device import DEFAULT_ITER_TIME
from archinstall.lib.models.users import Password
from archinstall.lib.output import debug, info
from archinstall.lib.utils.util import generate_password

View File

@ -7,6 +7,7 @@ from typing import Literal, overload
from archinstall.lib.command import SysCommand, SysCommandWorker
from archinstall.lib.disk.utils import udev_sync
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.models.device import (
LvmGroupInfo,
LvmPVInfo,
@ -17,7 +18,6 @@ from archinstall.lib.models.device import (
Size,
Unit,
)
from archinstall.lib.output import debug
def _lvm_info(

View File

@ -19,8 +19,8 @@ from archinstall.lib.models.device import (
Size,
Unit,
)
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
@ -479,7 +479,7 @@ class PartitioningList(ListManager[DiskSegment]):
sector_size = device_info.sector_size
text = tr('Selected free space segment on device {}:').format(device_info.path) + '\n\n'
free_space_table = FormattedOutput.as_table([free_space])
free_space_table = as_table([free_space])
prompt = text + free_space_table + '\n'
max_sectors = free_space.length.format_size(Unit.sectors, sector_size)

View File

@ -4,8 +4,8 @@ from pydantic import BaseModel
from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import DiskError, SysCallError
from archinstall.lib.log import debug, info, warn
from archinstall.lib.models.device import LsblkInfo
from archinstall.lib.output import debug, info, warn
class LsblkOutput(BaseModel):

View File

@ -1,8 +1,8 @@
from enum import Enum
from archinstall.lib.locale.utils import list_timezones
from archinstall.lib.log import warn
from archinstall.lib.menu.helpers import Confirmation, Input, Selection
from archinstall.lib.output import warn
from archinstall.lib.translationhandler import Language, tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType

View File

@ -27,11 +27,11 @@ from archinstall.lib.models.packages import Repository
from archinstall.lib.models.pacman import PacmanConfiguration
from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.network.network_menu import select_network
from archinstall.lib.output import FormattedOutput
from archinstall.lib.packages.packages import list_available_packages, select_additional_packages
from archinstall.lib.pacman.config import PacmanConfig
from archinstall.lib.pacman.pacman_menu import PacmanMenu
from archinstall.lib.translationhandler import Language, tr, translation_handler
from archinstall.lib.utils.format import as_table
from archinstall.tui.components import tui
from archinstall.tui.menu_item import MenuItem, MenuItemGroup, MsgLevelType, PreviewResult
@ -299,7 +299,7 @@ class GlobalMenu(AbstractMenu[None]):
if item.value:
network_config: NetworkConfiguration = item.value
if network_config.type == NicType.MANUAL:
output = FormattedOutput.as_table(network_config.nics)
output = as_table(network_config.nics)
else:
output = f'{tr("Network configuration")}:\n{network_config.type.display_msg()}'
@ -321,7 +321,7 @@ class GlobalMenu(AbstractMenu[None]):
output += f'{tr("Root password")}: {auth_config.root_enc_password.hidden()}\n'
if auth_config.users:
output += FormattedOutput.as_table(auth_config.users) + '\n'
output += as_table(auth_config.users) + '\n'
if auth_config.u2f_config:
u2f_config = auth_config.u2f_config
@ -631,7 +631,7 @@ class GlobalMenu(AbstractMenu[None]):
if mirror_config.custom_repositories:
title = tr('Custom repositories')
table = FormattedOutput.as_table(mirror_config.custom_repositories)
table = as_table(mirror_config.custom_repositories)
output += f'{title}:\n\n{table}'
return output.strip()

View File

@ -6,8 +6,8 @@ from typing import Self
from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.networking import enrich_iface_types, list_interfaces
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr

View File

@ -29,6 +29,7 @@ from archinstall.lib.exceptions import DiskError, HardwareIncompatibilityError,
from archinstall.lib.hardware import SysInfo
from archinstall.lib.linux_path import LPath
from archinstall.lib.locale.utils import verify_keyboard_layout, verify_x11_keyboard_layout
from archinstall.lib.log import debug, error, info, log, logger, warn
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
from archinstall.lib.models.application import ZramAlgorithm
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
@ -52,7 +53,6 @@ from archinstall.lib.models.package_types import DEFAULT_KERNEL, Kernel
from archinstall.lib.models.packages import Repository
from archinstall.lib.models.pacman import PacmanConfiguration
from archinstall.lib.models.users import User
from archinstall.lib.output import debug, error, info, log, logger, warn
from archinstall.lib.packages.packages import installed_package
from archinstall.lib.pacman.config import PacmanConfig
from archinstall.lib.pacman.pacman import Pacman
@ -375,7 +375,12 @@ class Installer:
# it would be none if it's btrfs as the subvolumes will have the mountpoints defined
if part_mod.mountpoint:
target = self.target / part_mod.relative_mountpoint
mount(part_mod.dev_path, target, options=part_mod.mount_options)
options = part_mod.mount_options
if part_mod.is_efi():
options = list(dict.fromkeys(options + ['fmask=0077', 'dmask=0077']))
mount(part_mod.dev_path, target, options=options)
elif part_mod.fs_type == FilesystemType.BTRFS:
# Only mount BTRFS subvolumes that have mountpoints specified
subvols_with_mountpoints = [sv for sv in part_mod.btrfs_subvols if sv.mountpoint is not None]

View File

@ -3,7 +3,7 @@ from pathlib import Path
from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import ServiceException, SysCallError
from archinstall.lib.output import error
from archinstall.lib.log import error
from archinstall.lib.utils.util import running_from_iso

259
archinstall/lib/log.py Normal file
View File

@ -0,0 +1,259 @@
import logging
import os
import sys
import urllib.error
import urllib.request
from enum import Enum
from pathlib import Path
from archinstall.lib.utils.util import timestamp
class Logger:
def __init__(self, path: Path | None = None) -> None:
if path is None:
path = Path('/var/log/archinstall')
self._path: Path = path
@property
def path(self) -> Path:
return self._path / 'install.log'
@path.setter
def path(self, value: Path) -> None:
self._path = value
@property
def directory(self) -> Path:
return self._path
def _check_permissions(self) -> None:
log_file = self.path
try:
self._path.mkdir(exist_ok=True, parents=True)
log_file.touch(exist_ok=True)
with log_file.open('a') as f:
f.write('')
except PermissionError:
# Fallback to creating the log file in the current folder
logger._path = Path('./').absolute()
warn(f'Not enough permission to place log file at {log_file}, creating it in {logger.path} instead')
def log(self, level: int, content: str) -> None:
self._check_permissions()
with self.path.open('a') as f:
ts = timestamp()
level_name = logging.getLevelName(level)
f.write(f'[{ts}] - {level_name} - {content}\n')
def get_content(self, max_bytes: int | None = None) -> bytes:
content = self.path.read_bytes()
if max_bytes is not None:
size = self.path.stat().st_size
if size > max_bytes:
content = content[-max_bytes:]
return content
logger = Logger()
def _supports_color() -> bool:
"""
Found first reference here:
https://stackoverflow.com/questions/7445658/how-to-detect-if-the-console-does-support-ansi-escape-codes-in-python
And re-used this:
https://github.com/django/django/blob/master/django/core/management/color.py#L12
Return True if the running system's terminal supports color,
and False otherwise.
"""
supported_platform = sys.platform != 'win32' or 'ANSICON' in os.environ
# isatty is not always implemented, #6223.
is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
return supported_platform and is_a_tty
class Font(Enum):
bold = '1'
italic = '3'
underscore = '4'
blink = '5'
reverse = '7'
conceal = '8'
def _stylize_output(
text: str,
fg: str,
bg: str | None,
reset: bool,
font: list[Font] = [],
) -> str:
"""
Heavily influenced by:
https://github.com/django/django/blob/ae8338daf34fd746771e0678081999b656177bae/django/utils/termcolors.py#L13
Color options here:
https://askubuntu.com/questions/528928/how-to-do-underline-bold-italic-strikethrough-color-background-and-size-i
Adds styling to a text given a set of color arguments.
"""
colors = {
'black': '0',
'red': '1',
'green': '2',
'yellow': '3',
'blue': '4',
'magenta': '5',
'cyan': '6',
'white': '7',
'teal': '8;5;109', # Extended 256-bit colors (not always supported)
'orange': '8;5;208', # https://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html#256-colors
'darkorange': '8;5;202',
'gray': '8;5;246',
'grey': '8;5;246',
'darkgray': '8;5;240',
'lightgray': '8;5;256',
}
foreground = {key: f'3{colors[key]}' for key in colors}
background = {key: f'4{colors[key]}' for key in colors}
code_list = []
if text == '' and reset:
return '\x1b[0m'
code_list.append(foreground[str(fg)])
if bg:
code_list.append(background[str(bg)])
for o in font:
code_list.append(o.value)
ansi = ';'.join(code_list)
return f'\033[{ansi}m{text}\033[0m'
def journal_log(message: str, level: int = logging.DEBUG) -> None:
try:
import systemd.journal # type: ignore[import-not-found]
except ModuleNotFoundError:
return
log_adapter = logging.getLogger('archinstall')
log_fmt = logging.Formatter('[%(levelname)s]: %(message)s')
log_ch = systemd.journal.JournalHandler()
log_ch.setFormatter(log_fmt)
log_adapter.addHandler(log_ch)
log_adapter.setLevel(logging.DEBUG)
log_adapter.log(level, message)
def info(
*msgs: str,
level: int = logging.INFO,
fg: str = 'white',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
def debug(
*msgs: str,
level: int = logging.DEBUG,
fg: str = 'white',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
def error(
*msgs: str,
level: int = logging.ERROR,
fg: str = 'red',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
def warn(
*msgs: str,
level: int = logging.WARNING,
fg: str = 'yellow',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
def log(
*msgs: str,
level: int = logging.INFO,
fg: str = 'white',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
text = ' '.join(str(x) for x in msgs)
logger.log(level, text)
# Attempt to colorize the output if supported
# Insert default colors and override with **kwargs
if _supports_color():
text = _stylize_output(text, fg, bg, reset, font)
journal_log(text, level=level)
if level != logging.DEBUG:
print(text)
def share_install_log(
paste_url: str,
max_bytes: int | None = None,
) -> str | None:
log_path = logger.path
if not log_path.exists():
info(f'Log file not found: {log_path}')
return None
content = logger.get_content(max_bytes=max_bytes)
if len(content) == 0:
info(f'Log file is empty: {log_path}')
return None
try:
req = urllib.request.Request(paste_url, data=content)
with urllib.request.urlopen(req) as response:
url = response.read().decode().strip()
except urllib.error.URLError as e:
info(f'Upload failed: {e}')
return None
if not url.startswith('http'):
info(f'Unexpected response from {paste_url}: {url[:200]!r}')
return None
return url

View File

@ -2,8 +2,8 @@ from enum import Enum
from types import TracebackType
from typing import Any, Self, override
from archinstall.lib.log import error
from archinstall.lib.menu.helpers import Selection
from archinstall.lib.output import error
from archinstall.lib.translationhandler import tr
from archinstall.tui.components import InstanceRunnable
from archinstall.tui.menu_item import MenuItem, MenuItemGroup

View File

@ -1,4 +1,4 @@
from archinstall.lib.output import FormattedOutput
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
@ -32,7 +32,7 @@ class MenuHelper[ValueT]:
display_data: dict[str, ValueT | str | None] = {}
if data:
table = FormattedOutput.as_table(data)
table = as_table(data)
rows = table.split('\n')
# these are the header rows of the table

View File

@ -2,10 +2,10 @@ import time
import urllib.parse
from pathlib import Path
from archinstall.lib.log import debug, info
from archinstall.lib.models import MirrorRegion
from archinstall.lib.models.mirrors import MirrorStatusEntryV3, MirrorStatusListV3
from archinstall.lib.networking import fetch_data_from_url
from archinstall.lib.output import debug, info
from archinstall.lib.pathnames import MIRRORLIST

View File

@ -13,8 +13,8 @@ from archinstall.lib.models.mirrors import (
SignOption,
)
from archinstall.lib.models.packages import Repository
from archinstall.lib.output import FormattedOutput
from archinstall.lib.translationhandler import tr
from archinstall.lib.utils.format import as_table
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.result import ResultType
@ -65,7 +65,7 @@ class CustomMirrorRepositoriesList(ListManager[CustomRepository]):
async def _add_custom_repository(self, preset: CustomRepository | None = None) -> CustomRepository | None:
edit_result = await Input(
header=tr('Enter a respository name'),
header=tr('Enter a repository name'),
allow_skip=True,
default_value=preset.name if preset else None,
).show()
@ -281,7 +281,7 @@ class MirrorMenu(AbstractSubMenu[MirrorConfiguration]):
return None
custom_mirrors: list[CustomRepository] = item.value
output = FormattedOutput.as_table(custom_mirrors)
output = as_table(custom_mirrors)
return output.strip()
def _prev_custom_servers(self, item: MenuItem) -> str | None:

View File

@ -3,8 +3,8 @@ from dataclasses import dataclass
from enum import Enum
from typing import Any, Self, override
from archinstall.lib.log import warn
from archinstall.lib.models.config import SubConfig
from archinstall.lib.output import warn
from archinstall.lib.translationhandler import tr

View File

@ -12,9 +12,9 @@ from parted import Disk, Geometry, Partition
from pydantic import BaseModel, Field, ValidationInfo, field_serializer, field_validator
from archinstall.lib.hardware import SysInfo
from archinstall.lib.log import debug
from archinstall.lib.models.config import SubConfig
from archinstall.lib.models.users import Password
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr
ENC_IDENTIFIER = 'ainst'

View File

@ -9,10 +9,10 @@ from typing import TYPE_CHECKING, Any, Self, TypedDict, override
from pydantic import BaseModel, ValidationInfo, field_validator, model_validator
from archinstall.lib.log import debug
from archinstall.lib.models.config import SubConfig
from archinstall.lib.models.packages import Repository
from archinstall.lib.networking import DownloadTimer, ping
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr
if TYPE_CHECKING:

View File

@ -3,8 +3,8 @@ from dataclasses import dataclass, field
from enum import Enum
from typing import NotRequired, Self, TypedDict, override
from archinstall.lib.log import debug
from archinstall.lib.models.config import SubConfig
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr
@ -12,6 +12,7 @@ class NicType(Enum):
ISO = 'iso'
NM = 'nm'
NM_IWD = 'nm_iwd'
IWD = 'iwd'
MANUAL = 'manual'
def display_msg(self) -> str:
@ -22,6 +23,8 @@ class NicType(Enum):
return tr('Use Network Manager (default backend)')
case NicType.NM_IWD:
return tr('Use Network Manager (iwd backend)')
case NicType.IWD:
return tr('Use standalone iwd')
case NicType.MANUAL:
return tr('Manual configuration')
@ -131,6 +134,10 @@ class NetworkConfiguration(SubConfig):
return cls(NicType.ISO)
case NicType.NM:
return cls(NicType.NM)
case NicType.NM_IWD:
return cls(NicType.NM_IWD)
case NicType.IWD:
return cls(NicType.IWD)
case NicType.MANUAL:
nics_arg = config.get('nics', [])
if nics_arg:

View File

@ -1,3 +1,5 @@
import textwrap
from archinstall.lib.installer import Installer
from archinstall.lib.models.network import NetworkConfiguration, NicType
from archinstall.lib.models.profile import ProfileConfiguration
@ -32,6 +34,13 @@ def install_network_config(
_configure_nm_iwd(installation)
installation.disable_service('iwd.service')
case NicType.IWD:
installation.add_additional_packages(['iwd'])
_configure_iwd_standalone(installation)
installation.enable_service('iwd.service')
installation.enable_service('systemd-networkd.service')
installation.enable_service('systemd-resolved.service')
case NicType.MANUAL:
for nic in network_config.nics:
installation.configure_nic(nic)
@ -45,3 +54,36 @@ def _configure_nm_iwd(installation: Installer) -> None:
iwd_backend_conf = nm_conf_dir / 'wifi_backend.conf'
_ = iwd_backend_conf.write_text('[device]\nwifi.backend=iwd\n')
def _configure_iwd_standalone(installation: Installer) -> None:
# iwd manages wireless only; systemd-networkd handles wired DHCP.
iwd_conf_dir = installation.target / 'etc/iwd'
iwd_conf_dir.mkdir(parents=True, exist_ok=True)
main_conf = iwd_conf_dir / 'main.conf'
main_conf_content = textwrap.dedent("""\
[General]
EnableNetworkConfiguration=true
[Network]
NameResolvingService=systemd
""")
_ = main_conf.write_text(main_conf_content)
networkd_dir = installation.target / 'etc/systemd/network'
networkd_dir.mkdir(parents=True, exist_ok=True)
wired_conf = networkd_dir / '20-wired.network'
wired_conf_content = textwrap.dedent("""\
[Match]
Type=ether
Kind=!*
[Network]
DHCP=yes
""")
_ = wired_conf.write_text(wired_conf_content)
resolv = installation.target / 'etc/resolv.conf'
resolv.unlink(missing_ok=True)
resolv.symlink_to('/run/systemd/resolve/stub-resolv.conf')

View File

@ -202,6 +202,8 @@ async def select_network(preset: NetworkConfiguration | None) -> NetworkConfigur
return NetworkConfiguration(NicType.NM)
case NicType.NM_IWD:
return NetworkConfiguration(NicType.NM_IWD)
case NicType.IWD:
return NetworkConfiguration(NicType.IWD)
case NicType.MANUAL:
preset_nics = preset.nics if preset else []
nics = await ManualNetworkConfig(tr('Configure interfaces'), preset_nics).show()

View File

@ -5,9 +5,9 @@ from typing import assert_never, override
from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.models.network import WifiConfiguredNetwork, WifiNetwork
from archinstall.lib.network.wpa_supplicant import WpaSupplicantConfig
from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr
from archinstall.tui.components import ConfirmationScreen, InputScreen, InstanceRunnable, LoadingScreen, NotifyScreen, TableSelectionScreen, tui
from archinstall.tui.menu_item import MenuItem, MenuItemGroup

View File

@ -1,8 +1,8 @@
from dataclasses import dataclass, field
from pathlib import Path
from archinstall.lib.log import debug
from archinstall.lib.models.network import WifiNetwork
from archinstall.lib.output import debug
@dataclass

View File

@ -13,7 +13,7 @@ from urllib.parse import urlencode
from urllib.request import urlopen
from archinstall.lib.exceptions import DownloadTimeout, SysCallError
from archinstall.lib.output import debug, error, info
from archinstall.lib.log import debug, error, info
from archinstall.lib.pacman.pacman import Pacman

View File

@ -1,415 +0,0 @@
import logging
import os
import sys
import urllib.error
import urllib.request
from collections.abc import Callable
from dataclasses import asdict, is_dataclass
from datetime import UTC, datetime
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any
from archinstall.lib.utils.encoding import unicode_ljust, unicode_rjust
from archinstall.tui.rich import BaseRichTable
if TYPE_CHECKING:
from _typeshed import DataclassInstance
class FormattedOutput:
@staticmethod
def _get_values(
o: DataclassInstance,
class_formatter: str | Callable | None = None, # type: ignore[type-arg] # pyright: ignore[reportMissingTypeArgument]
filter_list: list[str] = [],
) -> dict[str, Any]:
"""
the original values returned a dataclass as dict thru the call to some specific methods
this version allows thru the parameter class_formatter to call a dynamically selected formatting method.
Can transmit a filter list to the class_formatter,
"""
if class_formatter:
# if invoked per reference it has to be a standard function or a classmethod.
# A method of an instance does not make sense
if callable(class_formatter):
return class_formatter(o, filter_list)
# if is invoked by name we restrict it to a method of the class. No need to mess more
elif hasattr(o, class_formatter) and callable(getattr(o, class_formatter)):
func = getattr(o, class_formatter)
return func(filter_list)
raise ValueError('Unsupported formatting call')
elif hasattr(o, 'table_data'):
return o.table_data()
elif hasattr(o, 'json'):
return o.json()
elif is_dataclass(o):
return asdict(o)
else:
return o.__dict__ # type: ignore[unreachable]
@classmethod
def as_table(
cls,
obj: list[Any],
class_formatter: str | Callable | None = None, # type: ignore[type-arg]
filter_list: list[str] = [],
capitalize: bool = False,
) -> str:
"""variant of as_table (subtly different code) which has two additional parameters
filter which is a list of fields which will be shown
class_formatter a special method to format the outgoing data
A general comment, the format selected for the output (a string where every data record is separated by newline)
is for compatibility with a print statement
As_table_filter can be a drop in replacement for as_table
"""
raw_data = [cls._get_values(o, class_formatter, filter_list) for o in obj]
# determine the maximum column size
column_width: dict[str, int] = {}
for o in raw_data:
for k, v in o.items():
if not filter_list or k in filter_list:
column_width.setdefault(k, 0)
column_width[k] = max([column_width[k], len(str(v)), len(k)])
if not filter_list:
filter_list = list(column_width.keys())
# create the header lines
output = ''
key_list = []
for key in filter_list:
width = column_width[key]
key = key.replace('!', '').replace('_', ' ')
if capitalize:
key = key.capitalize()
key_list.append(unicode_ljust(key, width))
output += ' | '.join(key_list) + '\n'
output += '-' * len(output) + '\n'
# create the data lines
for record in raw_data:
obj_data = []
for key in filter_list:
width = column_width.get(key, len(key))
value = record.get(key, '')
if '!' in key:
value = '*' * len(value)
if isinstance(value, (int, float)) or (isinstance(value, str) and value.isnumeric()):
obj_data.append(unicode_rjust(str(value), width))
else:
obj_data.append(unicode_ljust(str(value), width))
output += ' | '.join(obj_data) + '\n'
return output
@staticmethod
def as_columns(entries: list[str], cols: int) -> str:
"""
Will format a list into a given number of columns
"""
chunks = []
output = ''
for i in range(0, len(entries), cols):
chunks.append(entries[i : i + cols])
for row in chunks:
out_fmt = '{: <30} ' * len(row)
output += out_fmt.format(*row) + '\n'
return output
def as_key_value_pair(
entries: dict[str, str | list[str] | bool],
ignore_empty: bool = True,
) -> str:
"""
Formats key-values as a Rich Table:
key1 : value1
key2 : value2
...
"""
table = BaseRichTable()
table.add_column('key', style='bold', no_wrap=True)
table.add_column('value', style='white', max_width=70)
for label, value in entries.items():
if ignore_empty and not value:
continue
if isinstance(value, bool):
value = 'Yes' if value else 'No'
if isinstance(value, list):
value = '\n '.join(str(val) for val in value)
table.add_row(label.title(), f': {value}')
return table.stringify()
class Journald:
@staticmethod
def log(message: str, level: int = logging.DEBUG) -> None:
try:
import systemd.journal # type: ignore[import-not-found]
except ModuleNotFoundError:
return
log_adapter = logging.getLogger('archinstall')
log_fmt = logging.Formatter('[%(levelname)s]: %(message)s')
log_ch = systemd.journal.JournalHandler()
log_ch.setFormatter(log_fmt)
log_adapter.addHandler(log_ch)
log_adapter.setLevel(logging.DEBUG)
log_adapter.log(level, message)
class Logger:
def __init__(self, path: Path = Path('/var/log/archinstall')) -> None:
self._path = path
@property
def path(self) -> Path:
return self._path / 'install.log'
@property
def directory(self) -> Path:
return self._path
def _check_permissions(self) -> None:
log_file = self.path
try:
self._path.mkdir(exist_ok=True, parents=True)
log_file.touch(exist_ok=True)
with log_file.open('a') as f:
f.write('')
except PermissionError:
# Fallback to creating the log file in the current folder
logger._path = Path('./').absolute()
warn(f'Not enough permission to place log file at {log_file}, creating it in {logger.path} instead')
def log(self, level: int, content: str) -> None:
self._check_permissions()
with self.path.open('a') as f:
ts = _timestamp()
level_name = logging.getLevelName(level)
f.write(f'[{ts}] - {level_name} - {content}\n')
logger = Logger()
def _supports_color() -> bool:
"""
Found first reference here:
https://stackoverflow.com/questions/7445658/how-to-detect-if-the-console-does-support-ansi-escape-codes-in-python
And re-used this:
https://github.com/django/django/blob/master/django/core/management/color.py#L12
Return True if the running system's terminal supports color,
and False otherwise.
"""
supported_platform = sys.platform != 'win32' or 'ANSICON' in os.environ
# isatty is not always implemented, #6223.
is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
return supported_platform and is_a_tty
class Font(Enum):
bold = '1'
italic = '3'
underscore = '4'
blink = '5'
reverse = '7'
conceal = '8'
def _stylize_output(
text: str,
fg: str,
bg: str | None,
reset: bool,
font: list[Font] = [],
) -> str:
"""
Heavily influenced by:
https://github.com/django/django/blob/ae8338daf34fd746771e0678081999b656177bae/django/utils/termcolors.py#L13
Color options here:
https://askubuntu.com/questions/528928/how-to-do-underline-bold-italic-strikethrough-color-background-and-size-i
Adds styling to a text given a set of color arguments.
"""
colors = {
'black': '0',
'red': '1',
'green': '2',
'yellow': '3',
'blue': '4',
'magenta': '5',
'cyan': '6',
'white': '7',
'teal': '8;5;109', # Extended 256-bit colors (not always supported)
'orange': '8;5;208', # https://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html#256-colors
'darkorange': '8;5;202',
'gray': '8;5;246',
'grey': '8;5;246',
'darkgray': '8;5;240',
'lightgray': '8;5;256',
}
foreground = {key: f'3{colors[key]}' for key in colors}
background = {key: f'4{colors[key]}' for key in colors}
code_list = []
if text == '' and reset:
return '\x1b[0m'
code_list.append(foreground[str(fg)])
if bg:
code_list.append(background[str(bg)])
for o in font:
code_list.append(o.value)
ansi = ';'.join(code_list)
return f'\033[{ansi}m{text}\033[0m'
def info(
*msgs: str,
level: int = logging.INFO,
fg: str = 'white',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
def _timestamp() -> str:
now = datetime.now(tz=UTC)
return now.strftime('%Y-%m-%d %H:%M:%S')
def debug(
*msgs: str,
level: int = logging.DEBUG,
fg: str = 'white',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
def error(
*msgs: str,
level: int = logging.ERROR,
fg: str = 'red',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
def warn(
*msgs: str,
level: int = logging.WARNING,
fg: str = 'yellow',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
def log(
*msgs: str,
level: int = logging.INFO,
fg: str = 'white',
bg: str | None = None,
reset: bool = False,
font: list[Font] = [],
) -> None:
text = ' '.join(str(x) for x in msgs)
logger.log(level, text)
# Attempt to colorize the output if supported
# Insert default colors and override with **kwargs
if _supports_color():
text = _stylize_output(text, fg, bg, reset, font)
Journald.log(text, level=level)
if level != logging.DEBUG:
print(text)
def share_install_log(
paste_url: str = 'https://paste.rs',
max_size: int = 10 * 1024 * 1024,
confirm: Callable[[str], bool] = lambda _: True,
) -> int:
log_path = logger.path
if not log_path.exists():
info(f'Log file not found: {log_path}')
return 1
size = log_path.stat().st_size
if size == 0:
info(f'Log file is empty: {log_path}')
return 1
if size > max_size:
info(f'Log file exceeds {max_size} bytes, uploading last {max_size} bytes')
content = log_path.read_bytes()[-max_size:]
else:
content = log_path.read_bytes()
header = f'About to upload {log_path} ({len(content)} bytes) to {paste_url}\n\n'
header += 'The log may contain hostname, mirror URLs, package list and partition layout.\n'
header += 'The uploaded paste is public.\n\n'
header += 'Continue?'
if not confirm(header):
info('Cancelled.')
return 1
try:
req = urllib.request.Request(paste_url, data=content)
with urllib.request.urlopen(req) as response:
url = response.read().decode().strip()
except urllib.error.URLError as e:
info(f'Upload failed: {e}')
return 1
if not url.startswith('http'):
info(f'Unexpected response from {paste_url}: {url[:200]!r}')
return 1
# raw print so the URL is pipe-friendly (no ANSI colors, no log prefix)
print(url)
return 0

View File

@ -1,9 +1,9 @@
from functools import lru_cache
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.log import debug
from archinstall.lib.menu.helpers import Loading, Notify, Selection
from archinstall.lib.models.packages import AvailablePackage, LocalPackage, PackageGroup, Repository
from archinstall.lib.output import debug
from archinstall.lib.pacman.pacman import Pacman
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup

View File

@ -1,6 +1,6 @@
from functools import lru_cache
from archinstall.lib.output import debug
from archinstall.lib.log import debug
from archinstall.lib.packages.packages import check_package_upgrade

View File

@ -5,7 +5,7 @@ from pathlib import Path
from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import RequirementError
from archinstall.lib.output import error, info, warn
from archinstall.lib.log import error, info, warn
from archinstall.lib.pathnames import PACMAN_CONF
from archinstall.lib.plugins import plugins
from archinstall.lib.translationhandler import tr

View File

@ -7,7 +7,7 @@ import urllib.request
from importlib import metadata
from pathlib import Path
from archinstall.lib.output import error, info, warn
from archinstall.lib.log import error, info, warn
from archinstall.lib.version import get_version
plugins = {}

View File

@ -9,9 +9,9 @@ from typing import TYPE_CHECKING, NotRequired, TypedDict
from archinstall.default_profiles.profile import CustomSetting, GreeterType, Profile
from archinstall.lib.hardware import GfxDriver, GfxPackage
from archinstall.lib.log import debug, error, info
from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.networking import fetch_data_from_url
from archinstall.lib.output import debug, error, info
from archinstall.lib.translationhandler import tr
if TYPE_CHECKING:
@ -175,6 +175,9 @@ class ProfileHandler:
case GreeterType.PlasmaLoginManager:
packages = ['plasma-login-manager']
service = ['plasmalogin']
case GreeterType.GreetdDms:
packages = ['greetd']
service = ['greetd']
if packages:
install_session.add_additional_packages(packages)
@ -194,6 +197,26 @@ class ProfileHandler:
with open(path, 'w') as file:
file.write(filedata)
if greeter == GreeterType.GreetdDms:
greetd_config = install_session.target / 'etc/greetd/config.toml'
greetd_config.parent.mkdir(parents=True, exist_ok=True)
greetd_config.write_text(
'[terminal]\n'
'vt = 1\n'
'\n'
'[default_session]\n'
'user = "greeter"\n'
'command = "/usr/share/quickshell/dms/Modules/Greetd/assets/dms-greeter --command niri -p /usr/share/quickshell/dms"\n',
)
tmpfiles = install_session.target / 'etc/tmpfiles.d/dms-greeter.conf'
tmpfiles.parent.mkdir(parents=True, exist_ok=True)
tmpfiles.write_text(
'# Path Mode User Group Age Argument\n'
'd /var/cache/dms-greeter 0750 greeter greeter -\n'
'd /var/lib/greeter 0755 greeter greeter -\n',
)
def install_gfx_driver(self, install_session: Installer, driver: GfxDriver) -> None:
debug(f'Installing GFX driver: {driver.value}')

View File

@ -9,7 +9,7 @@ from typing import override
from archinstall.lib.command import SysCommand
from archinstall.lib.exceptions import SysCallError
from archinstall.lib.output import debug
from archinstall.lib.log import debug
from archinstall.lib.utils.util import running_from_iso

View File

@ -0,0 +1,148 @@
from collections.abc import Callable
from dataclasses import asdict, is_dataclass
from typing import TYPE_CHECKING, Any
from archinstall.lib.utils.encoding import unicode_ljust, unicode_rjust
from archinstall.tui.rich import BaseRichTable
if TYPE_CHECKING:
from _typeshed import DataclassInstance
def as_key_value_pair(
entries: dict[str, str | list[str] | bool],
ignore_empty: bool = True,
) -> str:
"""
Formats key-values as a Rich Table:
key1 : value1
key2 : value2
...
"""
table = BaseRichTable()
table.add_column('key', style='bold', no_wrap=True)
table.add_column('value', style='white', max_width=70)
for label, value in entries.items():
if ignore_empty and not value:
continue
if isinstance(value, bool):
value = 'Yes' if value else 'No'
if isinstance(value, list):
value = '\n '.join(str(val) for val in value)
table.add_row(label.title(), f': {value}')
return table.stringify()
def as_columns(entries: list[str], cols: int) -> str:
"""
Will format a list into a given number of columns
"""
chunks: list[list[str]] = []
output = ''
for i in range(0, len(entries), cols):
chunks.append(entries[i : i + cols])
for row in chunks:
out_fmt = '{: <30} ' * len(row)
output += out_fmt.format(*row) + '\n'
return output
def _get_values(
o: DataclassInstance,
class_formatter: str | Callable | None = None, # type: ignore[type-arg] # pyright: ignore[reportMissingTypeArgument]
filter_list: list[str] = [],
) -> dict[str, Any]:
"""
the original values returned a dataclass as dict thru the call to some specific methods
this version allows thru the parameter class_formatter to call a dynamically selected formatting method.
Can transmit a filter list to the class_formatter,
"""
if class_formatter:
# if invoked per reference it has to be a standard function or a classmethod.
# A method of an instance does not make sense
if callable(class_formatter):
return class_formatter(o, filter_list)
# if is invoked by name we restrict it to a method of the class. No need to mess more
elif hasattr(o, class_formatter) and callable(getattr(o, class_formatter)):
func = getattr(o, class_formatter)
return func(filter_list)
raise ValueError('Unsupported formatting call')
elif hasattr(o, 'table_data'):
return o.table_data()
elif hasattr(o, 'json'):
return o.json()
elif is_dataclass(o):
return asdict(o)
else:
return o.__dict__ # type: ignore[unreachable]
def as_table(
obj: list[Any],
class_formatter: str | Callable | None = None, # type: ignore[type-arg]
filter_list: list[str] = [],
capitalize: bool = False,
) -> str:
"""variant of as_table (subtly different code) which has two additional parameters
filter which is a list of fields which will be shown
class_formatter a special method to format the outgoing data
A general comment, the format selected for the output (a string where every data record is separated by newline)
is for compatibility with a print statement
As_table_filter can be a drop in replacement for as_table
"""
raw_data = [_get_values(o, class_formatter, filter_list) for o in obj]
# determine the maximum column size
column_width: dict[str, int] = {}
for o in raw_data:
for k, v in o.items():
if not filter_list or k in filter_list:
column_width.setdefault(k, 0)
column_width[k] = max([column_width[k], len(str(v)), len(k)])
if not filter_list:
filter_list = list(column_width.keys())
# create the header lines
output = ''
key_list = []
for key in filter_list:
width = column_width[key]
key = key.replace('!', '').replace('_', ' ')
if capitalize:
key = key.capitalize()
key_list.append(unicode_ljust(key, width))
output += ' | '.join(key_list) + '\n'
output += '-' * len(output) + '\n'
# create the data lines
for record in raw_data:
obj_data = []
for key in filter_list:
width = column_width.get(key, len(key))
value = record.get(key, '')
if '!' in key:
value = '*' * len(value)
if isinstance(value, (int, float)) or (isinstance(value, str) and value.isnumeric()):
obj_data.append(unicode_rjust(str(value), width))
else:
obj_data.append(unicode_ljust(str(value), width))
output += ' | '.join(obj_data) + '\n'
return output

View File

@ -1,8 +1,14 @@
import secrets
import string
from datetime import UTC, datetime
from archinstall.lib.output import FormattedOutput
from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT
from archinstall.lib.utils.format import as_columns
def timestamp() -> str:
now = datetime.now(tz=UTC)
return now.strftime('%Y-%m-%d %H:%M:%S')
def running_from_iso() -> bool:
@ -36,7 +42,7 @@ def format_cols(items: list[str], header: str | None = None) -> str:
else:
col = 4
text += FormattedOutput.as_columns(items, col)
text += as_columns(items, col)
# remove whitespaces on each row
text = '\n'.join(t.strip() for t in text.split('\n'))
return text

View File

@ -1245,7 +1245,7 @@ msgid "Product"
msgstr ""
#, python-brace-format
msgid "Invalid configuration: {error}"
msgid "Invalid configuration: {}"
msgstr ""
msgid "Ready to install"
@ -2304,3 +2304,162 @@ msgstr ""
#, python-brace-format
msgid "Choose an option how to give {} access to your hardware"
msgstr ""
#, python-brace-format
msgid "About to upload \"{}\" to the publicly accessible {}"
msgstr ""
msgid "Do you want to continue?"
msgstr ""
#, python-brace-format
msgid "Log uploaded successfully. URL: {}"
msgstr ""
msgid "Failed to upload log."
msgstr ""
msgid "ArchInstall Language"
msgstr ""
msgid "Version"
msgstr ""
msgid "Installation Script"
msgstr ""
msgid "Application"
msgstr ""
msgid "Services"
msgstr ""
msgid "Custom commands"
msgstr ""
msgid "Users"
msgstr ""
msgid "Root encrypted password"
msgstr ""
msgid "Zram enabled"
msgstr ""
#, python-brace-format
msgid "Zram algorithm {}"
msgstr ""
msgid "Bluetooth enabled"
msgstr ""
#, python-brace-format
msgid "Audio server \"{}\""
msgstr ""
#, python-brace-format
msgid "Power management \"{}\""
msgstr ""
msgid "Print service enabled"
msgstr ""
#, python-brace-format
msgid "Firewall \"{}\""
msgstr ""
#, python-brace-format
msgid "Extra fonts \"{}\""
msgstr ""
msgid "Root password set"
msgstr ""
#, python-brace-format
msgid "Configured {} user(s)"
msgstr ""
msgid "U2F set up"
msgstr ""
#, python-brace-format
msgid "Bootloader \"{}\""
msgstr ""
msgid "UKI enabled"
msgstr ""
msgid "Default"
msgstr ""
msgid "Manual"
msgstr ""
msgid "Pre-mount"
msgstr ""
#, python-brace-format
msgid "{} layout"
msgstr ""
#, python-brace-format
msgid "Devices {}"
msgstr ""
msgid "LVM set up"
msgstr ""
#, python-brace-format
msgid "{} encryption"
msgstr ""
#, python-brace-format
msgid "Btrfs snapshot \"{}\""
msgstr ""
#, python-brace-format
msgid "Keyboard layout \"{}\""
msgstr ""
#, python-brace-format
msgid "Locale language \"{}\""
msgstr ""
#, python-brace-format
msgid "Locale encoding \"{}\""
msgstr ""
#, python-brace-format
msgid "Console font \"{}\""
msgstr ""
#, python-brace-format
msgid "Mirror regions \"{}\""
msgstr ""
#, python-brace-format
msgid "Optional repositories \"{}\""
msgstr ""
msgid "Custom servers set up"
msgstr ""
msgid "Custom repositories set up"
msgstr ""
msgid "Use standalone iwd"
msgstr ""
msgid "Color enabled"
msgstr ""
#, python-brace-format
msgid "{} grphics driver"
msgstr ""
#, python-brace-format
msgid "{} greeter"
msgstr ""
msgid "Enter a repository name"
msgstr ""

View File

@ -2190,7 +2190,7 @@ msgstr "Valittu työpöydän profiili vaati tavallisen käyttäjän kirjautumise
#, python-brace-format
msgid "Environment type: {} {}"
msgstr "Ympäristötyyppi: {}"
msgstr "Ympäristötyyppi: {} {}"
msgid "Input cannot be empty"
msgstr "Syöttö ei voi olla tyhjä"
@ -2245,4 +2245,4 @@ msgstr "Määritetään U2F laitetta käyttäjälle: {}"
#, python-brace-format
msgid "Default: {}ms, Recommended range: 1000-60000"
msgstr "Oletusarvo: 10000ms, suositeltu 1000-60000ms"
msgstr "Oletusarvo: {}ms, suositeltu 1000-60000ms"

View File

@ -1502,9 +1502,6 @@ msgstr "सक्षम"
msgid "Disabled"
msgstr "अक्षम"
msgid "Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues"
msgstr "कृपया इस समस्या (और फ़ाइल) को https://github.com/archlinux/archinstall/issues पर सबमिट करें।"
msgid "Mirror name"
msgstr "मिरर का नाम"

View File

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: 2026-04-12 10:26+0200\n"
"PO-Revision-Date: 2026-05-16 12:52+0200\n"
"Last-Translator: Van Matten\n"
"Language-Team: Alessio Cuccovillo <alessio.cuccovillo.dev@gmail.com>, Van Matten\n"
"Language: it\n"
@ -56,7 +56,7 @@ msgid "Only packages such as base, base-devel, linux, linux-firmware, efibootmgr
msgstr "Vengono installati solo pacchetti come base, base-devel, linux, linux-firmware, efibootmgr e pacchetti di profilo opzionali."
msgid "Note: base-devel is no longer installed by default. Add it here if you need build tools."
msgstr "Nota: base-devel non è più installato di default. Aggiungilo qui se ti servono i build tools."
msgstr "Nota: base-devel non è più installato come predefinito. Aggiungilo qui se ti servono i build tools."
msgid "If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt."
msgstr "Se desideri un browser web, come Firefox o Chromium, puoi specificarlo nel seguente prompt."
@ -118,7 +118,7 @@ msgid ""
msgstr ""
"{}\n"
"\n"
"Seleziona per indice quali partizioni eliminare"
"Seleziona per indice le partizioni da eliminare"
msgid ""
"{}\n"
@ -127,13 +127,13 @@ msgid ""
msgstr ""
"{}\n"
"\n"
"Seleziona per indice quale partizione montare dove"
"Seleziona per indice la partizione dove montare"
msgid " * Partition mount-points are relative to inside the installation, the boot would be /boot as an example."
msgstr " * I punti di mount della partizione sono relativi all'interno dell'installazione, per esempio l'avvio sarebbe /boot."
msgstr " * I punti di montaggio della partizione sono relativi all'interno dell'installazione, per esempio l'avvio sarebbe /boot."
msgid "Select where to mount partition (leave blank to remove mountpoint): "
msgstr "Seleziona dove montare la partizione (lascia vuoto per rimuovere il punto di mount): "
msgstr "Seleziona dove montare la partizione (lascia vuoto per rimuovere il punto di montaggio): "
msgid ""
"{}\n"
@ -142,7 +142,7 @@ msgid ""
msgstr ""
"{}\n"
"\n"
"Seleziona quale partizione mascherare per la formattazione"
"Seleziona la partizione da mascherare per la formattazione"
msgid ""
"{}\n"
@ -151,7 +151,7 @@ msgid ""
msgstr ""
"{}\n"
"\n"
"Seleziona quale partizione contrassegnare come crittografata"
"Seleziona la partizione da contrassegnare come crittografata"
msgid ""
"{}\n"
@ -160,7 +160,7 @@ msgid ""
msgstr ""
"{}\n"
"\n"
"Seleziona quale partizione contrassegnare come avviabile"
"Seleziona la partizione da contrassegnare come avviabile"
msgid ""
"{}\n"
@ -169,7 +169,7 @@ msgid ""
msgstr ""
"{}\n"
"\n"
"Seleziona su quale partizione impostare un filesystem"
"Seleziona la partizione su cui impostare un filesystem"
msgid "Enter a desired filesystem type for the partition: "
msgstr "Inserisci un tipo di filesystem desiderato per la partizione: "
@ -187,7 +187,7 @@ msgid "Select what you wish to do with the selected block devices"
msgstr "Seleziona cosa desideri fare con i dispositivi a blocchi selezionati"
msgid "This is a list of pre-programmed profiles, they might make it easier to install things like desktop environments"
msgstr "Questo è un elenco di profili pre-programmati, che potrebbero semplificare l'installazione di elementi come gli ambienti desktop"
msgstr "Questo è un elenco di profili preprogrammati, che potrebbero semplificare l'installazione di elementi come gli ambienti desktop"
msgid "Select keyboard layout"
msgstr "Seleziona il layout della tastiera"
@ -196,7 +196,7 @@ msgid "Select one of the regions to download packages from"
msgstr "Seleziona una delle regioni da cui scaricare i pacchetti"
msgid "Select one or more hard drives to use and configure"
msgstr "Seleziona uno o più dischi rigidi da utilizzare e configurare"
msgstr "Seleziona uno o più unità da utilizzare e configurare"
msgid "For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options."
msgstr "Per la migliore compatibilità con il tuo hardware AMD, potresti voler utilizzare tutte le opzioni open source o AMD / ATI."
@ -220,13 +220,13 @@ msgid "All open-source (default)"
msgstr "Tutti gli open source (predefinito)"
msgid "Choose which kernels to use or leave blank for default \"{}\""
msgstr "Scegli quali kernel usare o lascia vuoto per il predefinito \"{}\""
msgstr "Scegli i kernel da usare o lascia vuoto per il predefinito \"{}\""
msgid "Choose which locale language to use"
msgstr "Scegli quale lingua locale utilizzare"
msgstr "Scegli la lingua da usare"
msgid "Choose which locale encoding to use"
msgstr "Scegli quale codifica locale utilizzare"
msgstr "Scegli la codifica da usare"
msgid "Select one of the values shown below: "
msgstr "Seleziona uno dei valori mostrati di seguito: "
@ -235,7 +235,7 @@ msgid "Select one or more of the options below: "
msgstr "Seleziona una o più delle seguenti opzioni "
msgid "Adding partition...."
msgstr "Aggiungendo la partizione...."
msgstr "Aggiunta della partizione in corso..."
msgid "You need to enter a valid fs-type in order to continue. See `man parted` for valid fs-type's."
msgstr "Devi inserire un tipo di filesystem valido per continuare. Vedi `man parted` per tipi di filesystem validi."
@ -253,13 +253,16 @@ msgid "Mirror region"
msgstr "Regione dei mirror"
msgid "Locale language"
msgstr "Lingua locale"
msgstr "Lingua"
msgid "Locale encoding"
msgstr "Codifica locale"
msgstr "Codifica"
msgid "Console font"
msgstr "Font della console"
msgid "Drive(s)"
msgstr "Dischi"
msgstr "Unità"
msgid "Disk layout"
msgstr "Layout del disco"
@ -301,7 +304,7 @@ msgid "Automatic time sync (NTP)"
msgstr "Sincronizzazione automatica dell'ora (NTP)"
msgid "Install ({} config(s) missing)"
msgstr "Installa ({} configurazioni mancanti)"
msgstr "Installa ({} configurazione/i mancante/i)"
msgid ""
"You decided to skip harddrive selection\n"
@ -309,7 +312,7 @@ msgid ""
"WARNING: Archinstall won't check the suitability of this setup\n"
"Do you wish to continue?"
msgstr ""
"Hai deciso di saltare la selezione del disco rigido\n"
"Hai deciso di saltare la selezione dell'unità\n"
"e utilizzerà qualsiasi configurazione dell'unità montata su {} (sperimentale)\n"
"ATTENZIONE: Archinstall non verificherà l'idoneità di questa configurazione\n"
"Vuoi continuare?"
@ -327,7 +330,7 @@ msgid "Clear/Delete all partitions"
msgstr "Cancella/Elimina tutte le partizioni"
msgid "Assign mount-point for a partition"
msgstr "Assegna punto di mount per una partizione"
msgstr "Assegna punto di montaggio per una partizione"
msgid "Mark/Unmark a partition to be formatted (wipes data)"
msgstr "Seleziona/Deseleziona una partizione da formattare (cancella i dati)"
@ -376,7 +379,7 @@ msgid "Enter a encryption password for {}"
msgstr "Inserisci una password di crittografia per {}"
msgid "Enter disk encryption password (leave blank for no encryption): "
msgstr "Inserisci la password di crittografia del disco (lasciare vuoto per nessuna crittografia): "
msgstr "Inserisci la password di crittografia del disco (lascia vuoto per nessuna crittografia): "
msgid "Create a required super-user with sudo privileges: "
msgstr "Crea un superuser richiesto con privilegi sudo: "
@ -454,7 +457,7 @@ msgid "Pre-existing pacman lock never exited. Please clean up any existing pacma
msgstr "Il lock di pacman preesistente non è mai terminato. Rimuovi ogni sessione pacman esistente prima di usare archinstall."
msgid "Choose which optional additional repositories to enable"
msgstr "Scegli quali repository aggiuntivi facoltativi abilitare"
msgstr "Scegli i repository aggiuntivi facoltativi da abilitare"
msgid "Add a user"
msgstr "Aggiungi un utente"
@ -476,7 +479,7 @@ msgstr ""
"Definisci un nuovo utente\n"
msgid "User Name : "
msgstr "Nome utente : "
msgstr "Nome utente: "
msgid "Should {} be a superuser (sudoer)?"
msgstr "{} dovrebbe essere un superuser (sudoer)?"
@ -497,7 +500,7 @@ msgid ""
msgstr ""
"{}\n"
"\n"
"Seleziona su quale partizione impostare i sottovolumi"
"Seleziona la partizione su cui impostare i sottovolumi"
msgid "Manage btrfs subvolumes for current partition"
msgstr "Gestisci i sottovolumi btrfs per la partizione corrente"
@ -518,7 +521,7 @@ msgid "Save all"
msgstr "Salva tutto"
msgid "Choose which configuration to save"
msgstr "Scegli quale configurazione salvare"
msgstr "Scegli la configurazione da salvare"
msgid "Enter a directory for the configuration(s) to be saved: "
msgstr "Inserisci una cartella in cui salvare le configurazioni: "
@ -570,7 +573,7 @@ msgid "Subvolume name "
msgstr "Nome del sottovolume "
msgid "Subvolume mountpoint"
msgstr "Punto di mount del sottovolume"
msgstr "Punto di montaggio del sottovolume"
msgid "Subvolume options"
msgstr "Opzioni del sottovolume"
@ -579,10 +582,10 @@ msgid "Save"
msgstr "Salva"
msgid "Subvolume name :"
msgstr "Nome del sottovolume :"
msgstr "Nome del sottovolume:"
msgid "Select a mount point :"
msgstr "Seleziona un punto di mount:"
msgstr "Seleziona un punto di montaggio:"
msgid "Select the desired subvolume options "
msgstr "Seleziona le opzioni del sottovolume desiderate "
@ -607,10 +610,10 @@ msgid "The selected drives do not have the minimum capacity required for an auto
msgstr "Le unità selezionate non hanno la capacità minima richiesta per un suggerimento automatico\n"
msgid "Minimum capacity for /home partition: {}GB\n"
msgstr "Capacità minima per la partizione /home: {}GB\n"
msgstr "Capacità minima per la partizione /home: {} GB\n"
msgid "Minimum capacity for Arch Linux partition: {}GB"
msgstr "Capacità minima per la partizione Arch Linux: {}GB"
msgstr "Capacità minima per la partizione Arch Linux: {} GB"
msgid "Continue"
msgstr "Continua"
@ -661,13 +664,13 @@ msgid "Select your desired desktop environment"
msgstr "Seleziona l'ambiente desktop desiderato"
msgid "A very basic installation that allows you to customize Arch Linux as you see fit."
msgstr "Un'installazione molto semplice che ti consente di personalizzare Arch Linux come meglio credi."
msgstr "Un'installazione molto semplificata che ti consente di personalizzare Arch Linux come meglio credi."
msgid "Provides a selection of various server packages to install and enable, e.g. httpd, nginx, mariadb"
msgstr "Fornisce una selezione di vari pacchetti server da installare e abilitare, per esempio httpd, nginx, mariadb"
msgid "Choose which servers to install, if none then a minimal installation will be done"
msgstr "Scegli quali server installare, se nessuno verrà eseguita un'installazione minima"
msgstr "Scegli i server da installare, se non ne sarà selezionato nessuno, verrà eseguita un'installazione minima"
msgid "Installs a minimal system as well as xorg and graphics drivers."
msgstr "Installa un sistema minimo oltre a xorg e driver grafici."
@ -682,13 +685,13 @@ msgid "Are you sure you want to reset this setting?"
msgstr "Sei sicuro di voler ripristinare questa impostazione?"
msgid "Select one or more hard drives to use and configure\n"
msgstr "Seleziona uno o più dischi rigidi da utilizzare e configurare\n"
msgstr "Seleziona uno o più unità da utilizzare e configurare\n"
msgid "Any modifications to the existing setting will reset the disk layout!"
msgstr "Qualsiasi modifica all'impostazione esistente ripristinerà il layout del disco!"
msgid "If you reset the harddrive selection this will also reset the current disk layout. Are you sure?"
msgstr "Se si ripristina la selezione del disco rigido, verrà ripristinato anche il layout del disco corrente. Sei sicuro?"
msgstr "Se ripristini la selezione dell'unità, verrà ripristinato anche il layout del disco corrente. Sei sicuro?"
msgid "Save and exit"
msgstr "Salva ed esci"
@ -741,7 +744,7 @@ msgid "Select one of the disks or skip and use /mnt as default"
msgstr "Seleziona uno dei dischi o salta e usa /mnt come predefinito"
msgid "Select which partitions to mark for formatting:"
msgstr "Seleziona quali partizioni contrassegnare per la formattazione:"
msgstr "Seleziona le partizioni da contrassegnare per la formattazione:"
msgid "Use HSM to unlock encrypted drive"
msgstr "Utilizza HSM per sbloccare l'unità crittografata"
@ -798,14 +801,14 @@ msgid "Configured {} interfaces"
msgstr "Interfacce {} configurate"
msgid "This option enables the number of parallel downloads that can occur during installation"
msgstr "Questa opzione consente di impostare il numero di download paralleli che possono avvenire durante l'installazione"
msgstr "Questa opzione consente di impostare il numero di download in parallelo che possono avvenire durante l'installazione"
msgid ""
"Enter the number of parallel downloads to be enabled.\n"
" (Enter a value between 1 to {})\n"
"Note:"
msgstr ""
"Inserisci il numero di download paralleli da abilitare.\n"
"Inserisci il numero di download in parallelo da abilitare.\n"
" (Inserisci un valore compreso tra 1 e {})\n"
"Nota:"
@ -816,20 +819,33 @@ msgid " - Minimum value : 1 ( Allows 1 parallel download, allows 2 downloads a
msgstr " - Valore minimo : 1 ( Consente 1 download parallelo, consente 2 download alla volta )"
msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )"
msgstr " - Disabilita/Predefinito : 0 ( Disabilita il download parallelo, consente solo 1 download alla volta )"
msgstr " - Disabilita/Predefinito : 0 ( Disabilita il download in parallelo, consente solo 1 download alla volta )"
#, python-brace-format
msgid "Invalid input! Try again with a valid input [1 to {max_downloads}, or 0 to disable]"
msgstr "Input non valido! Riprova con un input valido [da 1 a {max_downloads}, o 0 per disabilitare]."
msgid "Parallel Downloads"
msgstr "Download paralleli"
msgstr "Download in parallelo"
msgid "Pacman"
msgstr "Pacman"
msgid "Color"
msgstr "Colore"
#, python-brace-format
msgid "Enter the number of parallel downloads (1-{})"
msgstr "Inserisci il numero di download in parallelo (1-{})"
msgid "Enable colored output for pacman"
msgstr "Attiva l'output colorato di pacman"
msgid "ESC to skip"
msgstr "ESC per saltare"
msgid "CTRL+C to reset"
msgstr "CTRL+C per resettare"
msgstr "CTRL+C per ripristinare"
msgid "TAB to select"
msgstr "TAB per selezionare"
@ -861,27 +877,27 @@ msgid "Select one or more devices to use and configure"
msgstr "Seleziona uno o più dispositivi da utilizzare e configurare"
msgid "If you reset the device selection this will also reset the current disk layout. Are you sure?"
msgstr "Se si ripristina la selezione del disco rigido, verrà ripristinato anche il layout del disco corrente. Sei sicuro?"
msgstr "Se ripristini la selezione del dispositivo, verrà ripristinato anche il layout del disco corrente. Sei sicuro?"
msgid "Existing Partitions"
msgstr "Partizioni esistenti"
msgid "Select a partitioning option"
msgstr "Selezione opzione di partizionamento"
msgstr "Seleziona un'opzione di partizionamento"
msgid "Enter the root directory of the mounted devices: "
msgstr "Inserisci la cartella principale dei dispositivi montati: "
#, python-brace-format
msgid "Minimum capacity for /home partition: {}GiB\n"
msgstr "Capacità minima per la partizione /home: {}GiB\n"
msgstr "Capacità minima per la partizione /home: {} GiB\n"
#, python-brace-format
msgid "Minimum capacity for Arch Linux partition: {}GiB"
msgstr "Capacità minima per la partizione Arch Linux: {}GiB"
msgstr "Capacità minima per la partizione Arch Linux: {} GiB"
msgid "This is a list of pre-programmed profiles_bck, they might make it easier to install things like desktop environments"
msgstr "Questo è un elenco di profili pre-programmati, che potrebbero semplificare l'installazione di elementi come gli ambienti desktop"
msgstr "Questo è un elenco di profiles_bck preprogrammati, che potrebbero semplificare l'installazione di elementi come gli ambienti desktop"
msgid "Current profile selection"
msgstr "Selezione profilo corrente"
@ -890,7 +906,7 @@ msgid "Remove all newly added partitions"
msgstr "Elimina tutte le partizioni appena aggiunte"
msgid "Assign mountpoint"
msgstr "Assegna punto di mount"
msgstr "Assegna punto di montaggio"
msgid "Mark/Unmark to be formatted (wipes data)"
msgstr "Seleziona/Deseleziona come da formattare (cancella i dati)"
@ -917,13 +933,13 @@ msgid "This partition is currently encrypted, to format it a filesystem has to b
msgstr "Questa partizione è attualmente crittografata, per formattarla è necessario specificare un filesystem"
msgid "Partition mount-points are relative to inside the installation, the boot would be /boot as an example."
msgstr "I punti di mount della partizione sono relativi all'interno dell'installazione, l'avvio sarebbe per esempio /boot."
msgstr "I punti di montaggio della partizione sono relativi all'interno dell'installazione, l'avvio sarebbe per esempio /boot."
msgid "If mountpoint /boot is set, then the partition will also be marked as bootable."
msgstr "Se il punto di mount /boot è impostato, anche la partizione sarà contrassegnata come avviabile."
msgstr "Se il punto di montaggio /boot è impostato, anche la partizione sarà contrassegnata come avviabile."
msgid "Mountpoint: "
msgstr "Punto di mount: "
msgstr "Punto di montaggio: "
msgid "Current free sectors on device {}:"
msgstr "Settori attualmente liberi sul dispositivo {}:"
@ -985,7 +1001,7 @@ msgid "Partitions to be encrypted"
msgstr "Partizioni da crittografare"
msgid "Select disk encryption option"
msgstr "Selezione opzione per crittografia disco"
msgstr "Seleziona un'opzione per la crittografia del disco"
msgid "Select a FIDO2 device to use for HSM"
msgstr "Seleziona un dispositivo FIDO2 da utilizzare per HSM"
@ -1028,7 +1044,7 @@ msgid "Back"
msgstr "Indietro"
msgid "Please chose which greeter to install for the chosen profiles: {}"
msgstr "Scegli quale greeter installare per i profili scelti: {}"
msgstr "Scegli il greeter da installare per i profili scelti: {}"
#, python-brace-format
msgid "Environment type: {}"
@ -1075,7 +1091,7 @@ msgstr ""
"Seleziona un driver grafico o lascia vuoto per installare tutti i driver open source"
msgid "Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)"
msgstr "Sway ha bisogno dellaccesso al tuo posto (insieme di dispositivi hardware, ad esempio tastiera, mouse, ecc.)"
msgstr "Sway richiede laccesso al tuo posto (insieme di dispositivi hardware, ad esempio tastiera, mouse, ecc.)"
msgid ""
"\n"
@ -1093,13 +1109,13 @@ msgid "Greeter"
msgstr "Greeter"
msgid "Please chose which greeter to install"
msgstr "Scegli quale greeter installare"
msgstr "Scegli il greeter da installare"
msgid "This is a list of pre-programmed default_profiles"
msgstr "Questo è un elenco di default_profiles preprogrammati"
msgid "Disk configuration"
msgstr "Configurazione disco"
msgstr "Configurazione del disco"
msgid "Profiles"
msgstr "Profili"
@ -1144,7 +1160,7 @@ msgid ""
"Enter a directory for the configuration(s) to be saved (tab completion enabled)\n"
"Save directory: "
msgstr ""
"Inserisci una cartella in cui salvare le configurazioni (completamento col tasto tab abilitato)\n"
"Inserisci una cartella in cui salvare le configurazioni (completamento col tasto tab attivo)\n"
"Cartella di salvataggio: "
msgid ""
@ -1166,7 +1182,7 @@ msgid "Mirror regions"
msgstr "Regione dei mirror"
msgid " - Maximum value : {} ( Allows {} parallel downloads, allows {max_downloads+1} downloads at a time )"
msgstr " - Valore massimo : {} ( Consente {} download paralleli, consente {max_downloads+1} downloads alla volta )"
msgstr " - Valore massimo : {} ( Consente {} download in parallelo, consente {max_downloads+1} download alla volta )"
msgid "Invalid input! Try again with a valid input [1 to {}, or 0 to disable]"
msgstr "Input non valido! Riprova con un input valido [da 1 a {}, o 0 per disabilitare]."
@ -1208,33 +1224,48 @@ msgstr "Prodotto"
msgid "Invalid configuration: {error}"
msgstr "Configurazione non valida: {error}"
msgid "Ready to install"
msgstr "Pronto per l'installazione"
msgid "Disks"
msgstr "Dischi"
msgid "Packages"
msgstr "Pacchetti"
msgid "Network"
msgstr "Rete"
msgid "Locale"
msgstr "Localizzazione"
msgid "Type"
msgstr "Tipo"
msgid "This option enables the number of parallel downloads that can occur during package downloads"
msgstr "Questa opzione consente di impostare il numero di download paralleli che possono avvenire durante l'installazione"
msgstr "Questa opzione consente di impostare il numero di download in parallelo che possono avvenire durante l'installazione"
msgid ""
"Enter the number of parallel downloads to be enabled.\n"
"\n"
"Note:\n"
msgstr ""
"Inserisci il numero di download paralleli da abilitare.\n"
"Inserisci il numero di download in parallelo da abilitare.\n"
"\n"
"Nota:\n"
#, python-brace-format
msgid " - Maximum recommended value : {} ( Allows {} parallel downloads at a time )"
msgstr " - Valore massimo raccomandato : {} ( Consente {} download paralleli)"
msgstr " - Valore massimo raccomandato : {} ( Consente {} download in parallelo)"
msgid " - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )\n"
msgstr " - Disabilita/Predefinito : 0 ( Disabilita il download parallelo, consente solo 1 download alla volta )\n"
msgstr " - Disabilita/Predefinito : 0 ( Disabilita il download in parallelo, consente solo 1 download alla volta )\n"
msgid "Invalid input! Try again with a valid input [or 0 to disable]"
msgstr "Input non valido! Riprova con un input valido [0 per disabilitare]."
msgid "Hyprland needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)"
msgstr "Hyprland ha bisogno dellaccesso al tuo posto (insieme di dispositivi hardware, ad esempio tastiera, mouse, ecc.)"
msgstr "Hyprland richiede laccesso al tuo posto (insieme di dispositivi hardware, ad esempio tastiera, mouse, ecc.)"
msgid ""
"\n"
@ -1270,7 +1301,7 @@ msgid "Selected profiles: "
msgstr "Profili selezionati: "
msgid "Time synchronization not completing, while you wait - check the docs for workarounds: https://archinstall.readthedocs.io/"
msgstr "La sincronizzazione dellorario non si sta completando, in attesa, leggi la documentazione alla ricerca di una soluzione: https://archinstall.readthedocs.io/"
msgstr "La sincronizzazione dellorario non si sta completando, mentre aspetti leggi la documentazione alla ricerca di una soluzione: https://archinstall.readthedocs.io/"
msgid "Mark/Unmark as nodatacow"
msgstr "Seleziona/Deseleziona come nodatacow"
@ -1322,7 +1353,7 @@ msgid "LVM volumes to be encrypted"
msgstr "Volumi LVM da crittografare"
msgid "Select which LVM volumes to encrypt"
msgstr "Seleziona volumi LVM da crittografare"
msgstr "Seleziona i volumi LVM da crittografare"
msgid "Default layout"
msgstr "Layout predefinito"
@ -1361,7 +1392,7 @@ msgid "Seat access"
msgstr "Accesso al posto"
msgid "Mountpoint"
msgstr "Punto di mount"
msgstr "Punto di montaggio"
msgid "HSM"
msgstr "HSM"
@ -1479,7 +1510,7 @@ msgid "Directory"
msgstr "Cartella"
msgid "Enter a directory for the configuration(s) to be saved (tab completion enabled)"
msgstr "Inserisci una cartella in cui salvare le configurazioni (completamento col tasto tab abilitato)"
msgstr "Inserisci una cartella in cui salvare le configurazioni (completamento col tasto tab attivo)"
#, python-brace-format
msgid "Do you want to save the configuration file(s) to {}?"
@ -1513,7 +1544,7 @@ msgid "Choose an option to give Hyprland access to your hardware"
msgstr "Scegli unopzione per concedere a Hyprland l'accesso al tuo hardware"
msgid "Additional repositories"
msgstr "Repository aggiuntive"
msgstr "Repository aggiuntivi"
msgid "NTP"
msgstr "NTP"
@ -1543,7 +1574,7 @@ msgid "HSM device"
msgstr "Dispositivo HSM"
msgid "Some packages could not be found in the repository"
msgstr "Alcuni pacchetti non sono stati trovati nella repository"
msgstr "Alcuni pacchetti non sono stati trovati nel repository"
msgid "User"
msgstr "Utente"
@ -1564,16 +1595,16 @@ msgid "Select any packages from the below list that should be installed addition
msgstr "Seleziona dalla lista sotto i pacchetti aggiuntivi da installare"
msgid "Add a custom repository"
msgstr "Aggiungi una repository personalizzata"
msgstr "Aggiungi un repository personalizzato"
msgid "Change custom repository"
msgstr "Cambia repository personalizzata"
msgstr "Cambia repository personalizzato"
msgid "Delete custom repository"
msgstr "Elimina repository personalizzata"
msgstr "Elimina repository personalizzato"
msgid "Repository name"
msgstr "Nome repository"
msgstr "Nome del repository"
msgid "Add a custom server"
msgstr "Aggiungi un server personalizzato"
@ -1594,7 +1625,7 @@ msgid "Add custom servers"
msgstr "Aggiungi server personalizzati"
msgid "Add custom repository"
msgstr "Aggiungi repository personalizzate"
msgstr "Aggiungi repository personalizzato"
msgid "Loading mirror regions..."
msgstr "Caricamento regioni dei mirror in corso..."
@ -1609,7 +1640,7 @@ msgid "Custom servers"
msgstr "Server personalizzati"
msgid "Custom repositories"
msgstr "Repository personalizzate"
msgstr "Repository personalizzati"
msgid "Only ASCII characters are supported"
msgstr "Sono supportati solo caratteri ASCII"
@ -1756,7 +1787,7 @@ msgid "Toggle option"
msgstr "Opzioni di attivazione"
msgid "Scroll Up"
msgstr "Scorri Su"
msgstr "Scorri su"
msgid "Scroll Down"
msgstr "Scorri giù"
@ -1783,13 +1814,13 @@ msgid "Copy selected text"
msgstr "Copia il testo selezionato"
msgid "labwc needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)"
msgstr "labwc ha bisogno dellaccesso al tuo posto (insieme di dispositivi hardware, ad esempio tastiera, mouse, ecc.)"
msgstr "labwc richiede laccesso al tuo posto (insieme di dispositivi hardware, ad esempio tastiera, mouse, ecc.)"
msgid "Choose an option to give labwc access to your hardware"
msgstr "Scegli unopzione per concedere a labwc laccesso al tuo hardware"
msgid "niri needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)"
msgstr "niri ha bisogno dellaccesso al tuo posto (insieme di dispositivi hardware, ad esempio tastiera, mouse, ecc.)"
msgstr "niri richiede laccesso al tuo posto (insieme di dispositivi hardware, ad esempio tastiera, mouse, ecc.)"
msgid "Choose an option to give niri access to your hardware"
msgstr "Scegli unopzione per concedere a niri laccesso al tuo hardware"
@ -1832,7 +1863,7 @@ msgid "Do you want to encrypt the user_credentials.json file?"
msgstr "Vuoi criptare il file di user_credentials.json?"
msgid "Credentials file encryption password"
msgstr "Password di crittazione del file delle credenziali"
msgstr "Password di crittografia del file delle credenziali"
#, python-brace-format
msgid "Repositories: {}"
@ -1917,7 +1948,7 @@ msgid "No network connection found"
msgstr "Nessuna connessione di rete trovata"
msgid "Would you like to connect to a Wifi?"
msgstr "Desideri connetterti ad una Wi-FI?"
msgstr "Desideri connetterti ad una Wi-Fi?"
msgid "No wifi interface found"
msgstr "Nessuna interfaccia Wi-Fi trovata"
@ -2000,11 +2031,26 @@ msgstr "Usa NetworkManager (backend iwd)"
msgid "Firewall"
msgstr "Firewall"
msgid "Additional fonts"
msgstr "Font aggiuntivi"
msgid "Select font packages to install"
msgstr "Seleziona i pacchetti di font da installare"
msgid "Unicode font coverage for most languages"
msgstr "Copertura Unicode del font per la maggior parte delle lingue"
msgid "color emoji for browsers and apps"
msgstr "emoji a colori per browser e app"
msgid "Chinese, Japanese, Korean characters"
msgstr "Caratteri cinesi, giapponesi, coreani"
msgid "Select audio configuration"
msgstr "Seleziona la configurazione audio"
msgid "Enter credentials file decryption password"
msgstr "Inserici la password di decrittazione del file delle credenziali"
msgstr "Inserisci la password di decrittazione del file delle credenziali"
msgid "Enter root password"
msgstr "Inserisci la password di root"
@ -2025,7 +2071,7 @@ msgid "Select disks for the installation"
msgstr "Seleziona il disco per l'installazione"
msgid "Enter a mountpoint"
msgstr "Inserisci un punto di mount"
msgstr "Inserisci un punto di montaggio"
#, python-brace-format
msgid "Enter a size (default: {}): "
@ -2035,7 +2081,7 @@ msgid "Enter subvolume name"
msgstr "Inserisci il nome del sottovolume"
msgid "Enter subvolume mountpoint"
msgstr "Inserisci il punto di mount del sottovolume"
msgstr "Inserisci il punto di montaggio del sottovolume"
msgid "Select a disk configuration"
msgstr "Seleziona una configurazione del disco"
@ -2047,7 +2093,7 @@ msgid "You will use whatever drive-setup is mounted at the specified directory"
msgstr "Userai qualunque configurazione del disco che sia montata nella cartella specificata"
msgid "WARNING: Archinstall won't check the suitability of this setup"
msgstr "ATTENZIONE: Archinstall non verificherà la compatibilità di di questa configurazione"
msgstr "ATTENZIONE: Archinstall non verificherà la compatibilità di questa configurazione"
msgid "Select main filesystem"
msgstr "Seleziona il filesystem principale"
@ -2066,22 +2112,22 @@ msgid "Value must be between 1 and {}"
msgstr "Il valore deve essere tra 1 e {}"
msgid "Select which kernel(s) to install"
msgstr "Scegli quali kernel installare"
msgstr "Seleziona i kernel da installare"
msgid "Enter a respository name"
msgstr "Inserisci il nome di un repository"
msgid "Enter the repository url"
msgstr "Inserisci l'URL della repository"
msgstr "Inserisci l'URL del repository"
msgid "Enter server url"
msgstr "Inserisci l'URL del server"
msgid "Select mirror regions to be enabled"
msgstr "Seleziona quali regioni dei mirror abilitare"
msgstr "Seleziona le regioni dei mirror da abilitare"
msgid "Select optional repositories to be enabled"
msgstr "Seleziona quali repository aggiuntive opzionali abilitare"
msgstr "Seleziona i repository opzionali da abilitare"
msgid "Select an interface"
msgstr "Seleziona un'interfaccia"
@ -2089,11 +2135,17 @@ msgstr "Seleziona un'interfaccia"
msgid "Choose network configuration"
msgstr "Scegli la configurazione di rete"
msgid "Recommended: Network Manager for desktop, Manual for server"
msgstr "Raccomandato: Network Manager per computer, Manuale per server"
msgid "Warning: no network configuration selected. Network will need to be set up manually on the installed system."
msgstr "Attenzione: nessuna configurazione di rete selezionata. La rete dovrà essere impostata manualmente sul sistema installato."
msgid "No packages found"
msgstr "Nessun pacchetto trovato"
msgid "Select which greeter to install"
msgstr "Seleziona quale greeter installare"
msgstr "Seleziona il greeter da installare"
msgid "Select a profile type"
msgstr "Seleziona un tipo di profilo"
@ -2126,6 +2178,9 @@ msgstr "Il profilo desktop selezionato richiede un utente regolare per accedere
msgid "Environment type: {} {}"
msgstr "Tipo di ambiente: {} {}"
msgid "Input cannot be empty"
msgstr "Il campo non può essere vuoto"
msgid "Recommended"
msgstr "Raccomandato"
@ -2156,5 +2211,194 @@ msgstr "Descrizione"
msgid "Select a flavor of KDE Plasma to install"
msgstr "Seleziona la configurazione di KDE Plasma da installare"
msgid "Input cannot be empty"
msgstr "Il campo non può essere vuoto"
msgid "Arial/Times/Courier replacement, Cyrillic support for Steam/games"
msgstr "Sostituto di Arial/Times/Courier, supporta il cirillico per Steam/giochi"
msgid "wide Unicode coverage, good fallback font"
msgstr "ampia copertura Unicode, ottimo font di riserva"
#, python-brace-format
msgid "Setting up U2F login: {u2f_config.u2f_login_method.value}"
msgstr "Impostazione accesso U2F: {u2f_config.u2f_login_method.value}"
#, python-brace-format
msgid "Default: {DEFAULT_ITER_TIME}ms, Recommended range: 1000-60000"
msgstr "Predefinito: {DEFAULT_ITER_TIME} ms, Intervallo consigliato: 1000-60000"
#, python-brace-format
msgid "Setting up U2F login: {}"
msgstr "Impostazione accesso U2F: {}"
#, python-brace-format
msgid "Default: {}ms, Recommended range: 1000-60000"
msgstr "Predefinito: {} ms, Intervallo consigliato: 1000-60000"
#, python-brace-format
msgid "{} needs access to your seat"
msgstr "{} richiede laccesso al tuo posto"
msgid "collection of hardware devices i.e. keyboard, mouse"
msgstr "insieme di dispositivi hardware, ad esempio tastiera e mouse"
#, python-brace-format
msgid "Choose an option how to give {} access to your hardware"
msgstr "Scegli unopzione per concedere a {} laccesso al tuo hardware"
#, python-brace-format
msgid "About to upload \"{}\" to the publicly accessible {}"
msgstr "\"{}\" sta per essere caricato per essere pubblicamente accessibile {}"
msgid "Do you want to continue?"
msgstr "Vuoi continuare?"
#, python-brace-format
msgid "Log uploaded successfully. URL: {}"
msgstr "Log caricato con successo. URL: {}"
msgid "Failed to upload log."
msgstr "Impossibile caricare il log."
msgid "ArchInstall Language"
msgstr "Lingua di ArchInstall"
msgid "Version"
msgstr "Versione"
msgid "Installation Script"
msgstr "Script di installazione"
msgid "Application"
msgstr "Applicazione"
msgid "Services"
msgstr "Servizi"
msgid "Custom commands"
msgstr "Comandi personalizzati"
msgid "Users"
msgstr "Utenti"
msgid "Root encrypted password"
msgstr "Password di root crittografata"
msgid "Zram enabled"
msgstr "Zram attiva"
#, python-brace-format
msgid "Zram algorithm {}"
msgstr "Algoritmo Zram {}"
msgid "Bluetooth enabled"
msgstr "Bluetooth attivo"
#, python-brace-format
msgid "Audio server \"{}\""
msgstr "Server audio \"{}\""
#, python-brace-format
msgid "Power management \"{}\""
msgstr "Gestione energetica \"{}\""
msgid "Print service enabled"
msgstr "Servizio di stampa attivo"
#, python-brace-format
msgid "Firewall \"{}\""
msgstr "Firewall \"{}\""
#, python-brace-format
msgid "Extra fonts \"{}\""
msgstr "Font aggiuntivi \"{}\""
msgid "Root password set"
msgstr "Imposta la password di root"
#, python-brace-format
msgid "Configured {} user(s)"
msgstr "{} utente/i configurato/i"
msgid "U2F set up"
msgstr "Imposta U2F"
#, python-brace-format
msgid "Bootloader \"{}\""
msgstr "Bootloader \"{}\""
msgid "UKI enabled"
msgstr "UKI attiva"
msgid "Default"
msgstr "Predefinito"
msgid "Manual"
msgstr "Manuale"
msgid "Pre-mount"
msgstr "Premontaggio"
#, python-brace-format
msgid "{} layout"
msgstr "Layout {}"
#, python-brace-format
msgid "Devices {}"
msgstr "Dispositivi {}"
msgid "LVM set up"
msgstr "Imposta LVM"
#, python-brace-format
msgid "{} encryption"
msgstr "Crittografia {}"
#, python-brace-format
msgid "Btrfs snapshot \"{}\""
msgstr "Snapshot Btrfs \"{}\""
#, python-brace-format
msgid "Keyboard layout \"{}\""
msgstr "Layout della tastiera \"{}\""
#, python-brace-format
msgid "Locale language \"{}\""
msgstr "Lingua \"{}\""
#, python-brace-format
msgid "Locale encoding \"{}\""
msgstr "Codifica \"{}\""
#, python-brace-format
msgid "Console font \"{}\""
msgstr "Font della console \"{}\""
#, python-brace-format
msgid "Mirror regions \"{}\""
msgstr "Regioni dei mirror \"{}\""
#, python-brace-format
msgid "Optional repositories \"{}\""
msgstr "Repository opzionali \"{}\""
msgid "Custom servers set up"
msgstr "Imposta server personalizzati"
msgid "Custom repositories set up"
msgstr "Imposta repository personalizzati"
msgid "Use standalone iwd"
msgstr "Usa iwd standalone"
msgid "Color enabled"
msgstr "Colore attivo"
#, python-brace-format
msgid "{} grphics driver"
msgstr "Driver grafici {}"
#, python-brace-format
msgid "{} greeter"
msgstr "Greeter {}"
msgid "Enter a repository name"
msgstr "Inserisci il nome di un repository"

File diff suppressed because it is too large Load Diff

View File

@ -1,48 +1,109 @@
#!/usr/bin/env bash
set -euo pipefail
cd $(dirname "$0")/..
cd "$(dirname "$0")/.."
function update_lang() {
file=${1}
usage() {
echo "Usage: ${0} <command>"
echo ""
echo "Commands:"
echo " all Regenerate base.pot and update all languages"
echo " <lang> Regenerate base.pot and update a single language"
echo " check Run translation validation checks"
echo " -h, --help Show this help"
}
generate_pot() {
find . -type f -iname '*.py' | sort \
| xargs xgettext --no-location --omit-header --keyword='tr' \
-d base -o locales/base.pot
}
update_lang() {
local file=${1}
echo "Updating: ${file}"
local path
path=$(dirname "${file}")
msgmerge --quiet --no-location --width 512 --backup none --update "${file}" locales/base.pot
msgfmt -o "${path}/base.mo" "${file}"
}
function generate_all() {
cmd_generate_all() {
generate_pot
for file in $(find locales/ -name "base.po"); do
update_lang "${file}"
done
}
function generate_single_lang() {
lang_file="locales/${1}/LC_MESSAGES/base.po"
cmd_generate_single() {
local lang_file="locales/${1}/LC_MESSAGES/base.po"
if [ ! -f "${lang_file}" ]; then
echo "Language files not found: ${lang_file}"
exit 1
fi
generate_pot
update_lang "${lang_file}"
}
cmd_check_po_syntax() {
echo "Checking .po syntax..."
local failed=0
while IFS= read -r po; do
if ! msgfmt --check --output-file=/dev/null "$po" 2>&1; then
echo "FAIL: $po"
failed=1
fi
done < <(find locales/ -name '*.po')
if [ "$failed" -eq 1 ]; then
echo "ERROR: some .po files have syntax errors" >&2
return 1
fi
echo "All .po files passed syntax check."
}
cmd_check_no_tr_fstring() {
echo "Checking for tr(f-string) anti-pattern..."
if grep -rnE "tr\(\s*f['\"]" . --include='*.py'; then
echo "ERROR: use tr('...{}').format(...) instead of tr(f'...')" >&2
return 1
fi
echo "No tr(f-string) anti-pattern found."
}
cmd_check_pot_freshness() {
# msgcmp (not diff) because base.pot carries legacy stale entries from
# --join-existing; diff would always fail until a full cleanup is done.
echo "Checking base.pot for missing strings..."
find . -type f -iname '*.py' | sort \
| xargs xgettext --no-location --omit-header --keyword='tr' \
-d base -o /tmp/generated.pot
if ! msgcmp --use-untranslated locales/base.pot /tmp/generated.pot; then
echo "ERROR: base.pot is missing strings - run: locales_generator.sh all" >&2
return 1
fi
echo "base.pot contains all translatable strings."
}
cmd_check() {
local failed=0
cmd_check_po_syntax || failed=1
cmd_check_no_tr_fstring || failed=1
cmd_check_pot_freshness || failed=1
if [ "$failed" -eq 1 ]; then
echo "Some translation checks failed." >&2
exit 1
fi
echo "All translation checks passed."
}
if [ $# -eq 0 ]; then
echo "Usage: ${0} <language_abbr>"
echo "Special case 'all' for <language_abbr> builds all languages."
usage
exit 1
fi
lang=${1}
# Update the base file containing all translatable strings
find . -type f -iname "*.py" | xargs xgettext --join-existing --no-location --omit-header --keyword='tr' -d base -o locales/base.pot
case "${lang}" in
"all") generate_all;;
*) generate_single_lang "${lang}"
case "${1}" in
check) cmd_check ;;
all) cmd_generate_all ;;
-h|--help) usage ;;
*) cmd_generate_single "${1}" ;;
esac

View File

@ -8,13 +8,13 @@ import time
import traceback
from pathlib import Path
from archinstall.lib.args import ArchConfigHandler
from archinstall.lib.args import ArchConfigHandler, SubCommand
from archinstall.lib.disk.utils import disk_layouts
from archinstall.lib.hardware import SysInfo
from archinstall.lib.log import debug, error, info, logger, share_install_log, warn
from archinstall.lib.menu.helpers import Confirmation
from archinstall.lib.network.wifi_handler import WifiHandler
from archinstall.lib.networking import ping
from archinstall.lib.output import debug, error, info, share_install_log, warn
from archinstall.lib.packages.util import check_version_upgrade
from archinstall.lib.pacman.pacman import Pacman
from archinstall.lib.translationhandler import tr, translation_handler
@ -75,17 +75,36 @@ def _list_scripts() -> str:
return '\n'.join(lines)
def _tui_confirm(header: str) -> bool:
async def _ask() -> bool:
def _share_log_command() -> None:
paste_url: str = 'https://paste.rs'
log_path = logger.path
max_size = 10 * 1024 * 1024 # max supported size by paste.rs
content = logger.get_content(max_bytes=max_size).decode()
header = tr('About to upload "{}" to the publicly accessible {}').format(log_path, paste_url) + '\n\n'
header += tr('Do you want to continue?')
group = MenuItemGroup.yes_no()
group.set_preview_for_all(lambda _: content)
async def _confirm() -> bool:
result = await Confirmation(
group=MenuItemGroup.yes_no(),
header=header,
allow_skip=False,
preset=False,
group=group,
preview_header='Log content',
preview_location='bottom',
).show()
return result.get_value()
return tui.run(_ask)
result = tui.run(_confirm)
if result is True:
res = share_install_log(paste_url=paste_url, max_bytes=max_size)
if res is not None:
info(tr('Log uploaded successfully. URL: {}').format(res))
else:
error(tr('Failed to upload log.'))
def run() -> int:
@ -94,15 +113,19 @@ def run() -> int:
OR straight as a module: python -m archinstall
In any case we will be attempting to load the provided script to be run from the scripts/ folder
"""
if 'share-log' in sys.argv:
return share_install_log(confirm=_tui_confirm)
arch_config_handler = ArchConfigHandler()
if '--help' in sys.argv or '-h' in sys.argv:
arch_config_handler.print_help()
return 0
match arch_config_handler.args.command:
case SubCommand.SHARE_LOG:
_share_log_command()
exit(0)
case None:
pass
script = arch_config_handler.get_script()
if script == 'list':

View File

@ -12,13 +12,13 @@ from archinstall.lib.disk.utils import disk_layouts
from archinstall.lib.general.general_menu import PostInstallationAction, select_post_installation
from archinstall.lib.global_menu import GlobalMenu
from archinstall.lib.installer import Installer, accessibility_tools_in_use, run_custom_user_commands
from archinstall.lib.log import debug, error, info
from archinstall.lib.menu.util import delayed_warning
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
from archinstall.lib.models import Bootloader
from archinstall.lib.models.device import DiskLayoutType, EncryptionType
from archinstall.lib.models.users import User
from archinstall.lib.network.network_handler import install_network_config
from archinstall.lib.output import debug, error, info
from archinstall.lib.packages.util import check_version_upgrade
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.lib.translationhandler import tr

View File

@ -4,12 +4,12 @@ from archinstall.lib.configuration import ConfigurationOutput
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.installer import Installer
from archinstall.lib.log import debug, error, info
from archinstall.lib.menu.util import delayed_warning
from archinstall.lib.models import Bootloader
from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.models.users import Password, User
from archinstall.lib.network.network_handler import install_network_config
from archinstall.lib.output import debug, error, info
from archinstall.lib.profile.profiles_handler import profile_handler
from archinstall.lib.translationhandler import tr
from archinstall.tui.components import tui

View File

@ -7,8 +7,8 @@ from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.disk.utils import disk_layouts
from archinstall.lib.global_menu import GlobalMenu
from archinstall.lib.installer import Installer
from archinstall.lib.log import debug, error
from archinstall.lib.menu.util import delayed_warning
from archinstall.lib.output import debug, error
from archinstall.lib.translationhandler import tr
from archinstall.tui.components import tui

View File

@ -19,7 +19,7 @@ from textual.widgets.option_list import Option
from textual.widgets.selection_list import Selection
from textual.worker import WorkerCancelled
from archinstall.lib.output import debug
from archinstall.lib.log import debug
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup, MsgLevelType, PreviewResult
from archinstall.tui.result import Result, ResultType

View File

@ -21,7 +21,7 @@ dependencies = [
"pyparted==3.13.0",
"pydantic==2.13.4",
"cryptography==48.0.0",
"textual==8.2.5",
"textual==8.2.7",
"markdown-it-py==4.0.0",
"linkify-it-py==2.1.0",
]
@ -37,9 +37,10 @@ dev = [
"mypy==2.1.0",
"flake8==7.3.0",
"pre-commit==4.6.0",
"ruff==0.15.12",
"ruff==0.15.14",
"pylint==4.0.5",
"pytest==9.0.3",
"hypothesis>=6.152.4",
]
doc = ["sphinx"]
@ -60,6 +61,7 @@ include-package-data = true
"**/*.po",
"**/*.pot",
"**/*.json",
"**/*.kdl",
]
[tool.setuptools.package-dir]
@ -68,7 +70,10 @@ archinstall = "archinstall"
[tool.mypy]
python_version = "3.14"
files = "."
exclude = "^build/"
exclude = [
"^build/",
"^test_tooling/",
]
disallow_any_explicit = false
disallow_any_expr = false
disallow_any_unimported = true

View File

@ -0,0 +1,12 @@
# To build
mkosi build -B
# To run
mkosi qemu \
--drive=archinstall_small:25G \
-- \
-device nvme,serial=archinstall_small,drive=archinstall_small
*note: in order to boot the installation, we need to disable UKI being added to -kernel. I don't know of a way to do this yet, unless we tinker with mkosi/qemu.py - https://github.com/Torxed/mkosi/commit/6f3c20802bd73f88b672cc96ef0db1e542084316 - in which case we could do: `mkosi qemu --drive=archinstall_small:25G -- -device nvme,serial=archinstall_small,drive=archinstall_small,bootindex=0 -kernel none` but it still won't boot properly.*

View File

View File

@ -0,0 +1,50 @@
[Distribution]
Distribution=arch
# LocalMirror=file:///var/lib/localmirror
[Output]
Format=uki
# Format=disk
[Include]
Include=mkosi-vm
[Validation]
SecureBoot=false
SecureBootAutoEnroll=false
Sign=false
# Signing artifacts (hashsums etc) using a GPG key:
# Key=D4B58E897A929F2E
# Signing secure boot using PIV on yubikey:
# SecureBoot=true
# SecureBootKey=pkcs11:
# SecureBootCertificate=secureboot.crt
[Content]
Packages=
pacman
archlinux-keyring
amd-ucode
intel-ucode
# tpm2-tss
# libfido2
# libp11-kit
WithDocs=false
RootPassword=toor
Timezone=Europe/Stockholm
Keymap=sv-latin1
InitrdProfiles=network
[Config]
Profiles=archinstall
[Build]
Incremental=true
ToolsTree=default
ToolsTreeProfiles=devel,misc,package-manager,runtime,gui
WithNetwork=yes
[Runtime]
Console=gui
CPUs=4
RAM=8G

View File

@ -0,0 +1,24 @@
[Match]
# Matching with "Type=ether" causes issues with containers because it also matches virtual Ethernet interfaces (veth*).
# See https://bugs.archlinux.org/task/70892
# Instead match by globbing the network interface name.
Name=en*
Name=eth*
[Link]
RequiredForOnline=routable
[Network]
DHCP=yes
MulticastDNS=yes
# systemd-networkd does not set per-interface-type default route metrics
# https://github.com/systemd/systemd/issues/17698
# Explicitly set route metric, so that Ethernet is preferred over Wi-Fi and Wi-Fi is preferred over mobile broadband.
# Use values from NetworkManager. From nm_device_get_route_metric_default in
# https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/main/src/core/devices/nm-device.c
[DHCPv4]
RouteMetric=100
[IPv6AcceptRA]
RouteMetric=100

View File

@ -0,0 +1,3 @@
[Service]
ExecStart=
ExecStart=-/usr/bin/agetty --noreset --noclear --autologin root - ${TERM}

View File

@ -0,0 +1,5 @@
cd archinstall-git
rm -rf dist
uv build --no-build-isolation --wheel
uv pip install dist/*.whl --break-system-packages --system --no-build --no-deps

View File

@ -0,0 +1 @@
enable systemd-networkd.service

View File

View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
git clone https://github.com/archlinux/archinstall.git /root/archinstall-git
(cd /root/archinstall-git && git checkout master)
# TODO: Set geo-mirrors statically instead
curl -s "https://archlinux.org/mirrorlist/?country=SE&protocol=https&use_mirror_status=on" | sed -e 's/^#Server/Server/' -e '/^#/d' | rankmirrors -n 5 - > /etc/pacman.d/mirrorlist
pacman-key --init
pacman-key --populate archlinux

View File

@ -0,0 +1,56 @@
[Content]
Packages=
acpid
intel-media-driver
linux-firmware
linux-firmware-intel
ca-certificates-mozilla
ca-certificates-utils
nano
gvfs
noto-fonts
cantarell-fonts
ttf-dejavu
polkit
bash
bzip2
coreutils
file
filesystem
findutils
gawk
gcc-libs
gettext
glibc
grep
gzip
iproute2
iputils
licenses
pciutils
procps-ng
psmisc
sed
tar
linux
diffutils
less
strace
util-linux
xz
pacman-contrib
gcc
git
pkgconfig
python
python-pip
python-uv
python-setuptools
python-pyparted
python-pydantic
python-textual
dosfstools
btrfs-progs
arch-install-scripts
WithDocs=false
KernelCommandLine=quiet splash

View File

@ -0,0 +1 @@
13

View File

@ -0,0 +1,18 @@
# Qemu helper
Can be used with the `mkosi` test tooling
After `mkosi -B build` has been executed, run the following:
python test_tooling/qemu/qemu.py \
--uki ./test_tooling/mkosi/mkosi.output/image_13.efi \
--harddrive ~/test.qcow2:15G \
--harddrive ~/test_large.qcow2:25G
And install using `archinstall`, after the machine has been shutdown, run:
python test_tooling/qemu/qemu.py \
--harddrive ~/test.qcow2:15G \
--harddrive ~/test_large.qcow2:25G
As this will boot EFI mode with just the harddrives to verify the installation.

483
test_tooling/qemu/qemu.py Normal file
View File

@ -0,0 +1,483 @@
import getpass
import grp
import hashlib
import os
import pathlib
import re
import shlex
import shutil
import subprocess
import sys
import time
from argparse import ArgumentParser
from collections.abc import Iterator
from select import EPOLLHUP, EPOLLIN, epoll
from shutil import which
from types import TracebackType
from typing import Any, Self, override
class RequirementError(Exception):
pass
class ArgumentError(Exception):
pass
def get_master(interface):
master_path = pathlib.Path(f'/sys/class/net/{interface}/master')
return master_path.readlink().name if master_path.exists() else None
def gray(text):
return f'\033[38;5;246m{text}\033[0m'
def orange(text):
return f'\033[38;5;208m{text}\033[0m'
def red(text):
return f'\033[31m{text}\033[0m'
sudo_password = None # Gets populated later
harddrives = {}
username = getpass.getuser()
groupname = grp.getgrgid(os.getgid()).gr_name
# https://stackoverflow.com/a/43627833/929999
_VT100_ESCAPE_REGEX = r'\x1B\[[?0-9;]*[a-zA-Z]'
_VT100_ESCAPE_REGEX_BYTES = _VT100_ESCAPE_REGEX.encode()
parser = ArgumentParser(description='A set of common parameters for the tooling', add_help=True)
# Defaults to the order of which the harddrives are defined.
boot_option = parser.add_mutually_exclusive_group()
boot_option.add_argument('--uki', help='Boot a UKI (EFI) image')
boot_option.add_argument('--kernel', help='Boot a Linux kernel')
boot_option.add_argument('--iso', help='Boot a ISO 9660')
networking = parser.add_argument_group('Networking', "Disables the default '-net nic -net user' network behavior of Qemu.")
networking.add_argument('--tap', nargs='?', help='Configures a TAP interface and passes it in as a virtio-net-pci.', default=None, type=str)
networking.add_argument('--tap-mac', nargs='?', help='MAC for the --tap interface', default='52:54:00:00:00:02')
networking.add_argument('--bridge', nargs='?', help='Configures a bridge, to which the --tap is added.', default=None, type=str)
networking.add_argument('--bridge-mac', nargs='?', help='MAC for the interface', default=None)
networking.add_argument('--bridge-master', nargs='?', help="Which interface to set as 'master' on the bridge.", default=None, type=str)
hardware = parser.add_argument_group('Hardware', 'General hardware specs for the virtual machine')
# To override the use of EFI boot (will not work with --uki for obvious reasons)
hardware.add_argument('--bios', action='store_true', help='Disables EFI (edk2/ovmf) and uses BIOS support instead', default=False)
hardware.add_argument('--memory', nargs='?', help='Ammount of memory to supply the machine', default=8192)
hardware.add_argument('--harddrive', action='append', help='Sets up one or more virtio-scsi-pci, size is defined by --harddrive test.qcow2:15G', type=str)
hardware.add_argument('--cpu', help='Sets the number of cores to allocate (default nproc -1)', type=str, default=os.cpu_count() - 1 if os.cpu_count() else 1)
hardware.add_argument('--resolution', help="Sets Qemu's VGA resolution", type=str, default='1920x1107')
kernel = parser.add_argument_group('Kernel', '--kernel specific arguments')
kernel.add_argument('--initrd', nargs='?', help='Defines which ISO to run (skips build all together)', default=None, type=pathlib.Path)
args, unknowns = parser.parse_known_args() # pylint: disable=redefined-outer-name
if args.bios and args.uki:
raise ArgumentError('Cannot boot a --uki image with --bios mode (at least not that I know of).')
if args.uki is None and args.kernel is None and args.iso is None and args.harddrive is None:
raise ArgumentError('Cannot boot this machine, define at least one of: --uki, --kernel, --iso, --harddrive')
if args.bridge is None and args.bridge_master:
raise ArgumentError('Cannot use --bridge-master without defining --bridge')
if args.bridge is None and args.bridge_mac:
raise ArgumentError('Cannot use --bridge-mac without defining --bridge')
elif args.bridge and args.bridge_mac is None:
args.bridge_mac = '52:54:00:00:00:1'
if args.tap and not args.bridge and get_master(args.tap) is None:
# We'll allow it, because maybe we're tesing what happens without networking, but the NIC exists.
# Or the user has some creative iptables/nftables forwarding.
print(orange('--tap does not have a master, consider adding --bridge or manual set a master using ip-link(8).'))
if args.tap is None and args.bridge:
print(orange("--bridge* arguments will be ignored since there's no --tap defined"))
elif args.tap and args.tap_mac is None:
args.tap_mac = '52:54:00:00:00:2'
class SysCallError(Exception):
def __init__(self, message: str, exit_code: int | None = None, worker_log: bytes = b'') -> None:
super().__init__(message)
self.message = message
self.exit_code = exit_code
self.worker_log = worker_log
def clear_vt100_escape_codes(data: bytes) -> bytes:
return re.sub(_VT100_ESCAPE_REGEX_BYTES, b'', data)
def locate_binary(name: str) -> str:
if path := which(name):
return path
raise RequirementError(f'Binary {name} does not exist.')
def _pid_exists(pid: int) -> bool:
try:
return any(subprocess.check_output(['ps', '--no-headers', '-o', 'pid', '-p', str(pid)]).strip())
except subprocess.CalledProcessError:
return False
class SysCommandWorker:
def __init__(
self,
cmd: str | list[str],
peek_output: bool | None = False,
environment_vars: dict[str, str] | None = None,
working_directory: str = './',
remove_vt100_escape_codes_from_lines: bool = True,
):
if isinstance(cmd, str):
cmd = shlex.split(cmd)
if cmd and not cmd[0].startswith(('/', './')): # Path() does not work well
cmd[0] = locate_binary(cmd[0])
self.cmd = cmd
self.peek_output = peek_output
# define the standard locale for command outputs. For now the C ascii one. Can be overridden
self.environment_vars = {'LC_ALL': 'C'}
if environment_vars:
self.environment_vars.update(environment_vars)
self.working_directory = working_directory
self.exit_code: int | None = None
self._trace_log = b''
self._trace_log_pos = 0
self.poll_object = epoll()
self.child_fd: int | None = None
self.started = False
self.ended = False
self.remove_vt100_escape_codes_from_lines: bool = remove_vt100_escape_codes_from_lines
def __contains__(self, key: bytes) -> bool:
"""
Contains will also move the current buffert position forward.
This is to avoid re-checking the same data when looking for output.
"""
assert isinstance(key, bytes)
index = self._trace_log.find(key, self._trace_log_pos)
if index >= 0:
self._trace_log_pos += index + len(key)
return True
return False
def __iter__(self, *args: str, **kwargs: dict[str, Any]) -> Iterator[bytes]: # pylint: disable=redefined-outer-name
last_line = self._trace_log.rfind(b'\n')
lines = filter(None, self._trace_log[self._trace_log_pos : last_line].splitlines())
for line in lines:
if self.remove_vt100_escape_codes_from_lines:
line = clear_vt100_escape_codes(line)
yield line + b'\n'
self._trace_log_pos = last_line
@override
def __repr__(self) -> str:
self.make_sure_we_are_executing()
return str(self._trace_log)
@override
def __str__(self) -> str:
try:
return self._trace_log.decode('utf-8')
except UnicodeDecodeError:
return str(self._trace_log)
def __enter__(self) -> Self:
return self
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
# b''.join(sys_command('sync')) # No need to, since the underlying fs() object will call sync.
# TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager
if self.child_fd:
try:
os.close(self.child_fd)
except Exception:
pass
if self.peek_output:
# To make sure any peaked output didn't leave us hanging
# on the same line we were on.
sys.stdout.write('\n')
sys.stdout.flush()
if exc_type is not None:
print(gray(str(exc_value)))
if self.exit_code != 0:
raise SysCallError(
f'{self.cmd} exited with abnormal exit code [{self.exit_code}]: {str(self)[-500:]}',
self.exit_code,
worker_log=self._trace_log,
)
def is_alive(self) -> bool:
self.poll()
if self.started and not self.ended:
return True
return False
def write(self, data: bytes, line_ending: bool = True) -> int:
assert isinstance(data, bytes) # TODO: Maybe we can support str as well and encode it
self.make_sure_we_are_executing()
if self.child_fd:
return os.write(self.child_fd, data + (b'\n' if line_ending else b''))
return 0
def make_sure_we_are_executing(self) -> bool:
if not self.started:
return self.execute()
return True
def tell(self) -> int:
self.make_sure_we_are_executing()
return self._trace_log_pos
def seek(self, pos: int) -> None:
self.make_sure_we_are_executing()
# Safety check to ensure 0 < pos < len(tracelog)
self._trace_log_pos = min(max(0, pos), len(self._trace_log))
def peak(self, output: str | bytes) -> bool:
if self.peek_output:
if isinstance(output, bytes):
try:
output = output.decode('UTF-8')
except UnicodeDecodeError:
return False
sys.stdout.write(output)
sys.stdout.flush()
return True
def poll(self) -> None:
self.make_sure_we_are_executing()
if self.child_fd:
got_output = False
for _fileno, _event in self.poll_object.poll(0.1):
try:
output = os.read(self.child_fd, 8192)
got_output = True
self.peak(output)
self._trace_log += output
except OSError:
self.ended = True
break
if self.ended or (not got_output and not _pid_exists(self.pid)):
self.ended = True
try:
wait_status = os.waitpid(self.pid, 0)[1]
self.exit_code = os.waitstatus_to_exitcode(wait_status)
except ChildProcessError:
try:
wait_status = os.waitpid(self.child_fd, 0)[1]
self.exit_code = os.waitstatus_to_exitcode(wait_status)
except ChildProcessError:
self.exit_code = 1
def execute(self) -> bool:
import pty
if (old_dir := os.getcwd()) != self.working_directory:
os.chdir(str(self.working_directory))
# Note: If for any reason, we get a Python exception between here
# and until os.close(), the traceback will get locked inside
# stdout of the child_fd object. `os.read(self.child_fd, 8192)` is the
# only way to get the traceback without losing it.
self.pid, self.child_fd = pty.fork()
# https://stackoverflow.com/questions/4022600/python-pty-fork-how-does-it-work
if not self.pid:
try:
os.execve(self.cmd[0], list(self.cmd), {**os.environ, **self.environment_vars})
except FileNotFoundError:
print(red(f'{self.cmd[0]} does not exist.'))
self.exit_code = 1
return False
else:
# Only parent process moves back to the original working directory
os.chdir(old_dir)
self.started = True
self.poll_object.register(self.child_fd, EPOLLIN | EPOLLHUP)
return True
def decode(self, encoding: str = 'UTF-8') -> str:
return self._trace_log.decode(encoding)
def ensure_sudo():
global sudo_password # pylint: disable=global-statement
if sudo_password is None:
if (sudo_password := getpass.getpass(f'[sudo] password for {username}: ')) == '':
raise ValueError('Certain commands need sudo to work and no sudo password was given.')
def setup_networking():
if args.tap:
if pathlib.Path(f'/sys/class/net/{args.tap}').exists() is False:
print(gray(f'Creating {args.tap} for user {username} and group {groupname}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip tuntap add dev {args.tap} mode tap user {username} group {groupname}'), False
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
pw_prompted = True
if args.bridge:
if pathlib.Path(f'/sys/class/net/{args.bridge}').exists() is False:
print(gray(f'Creating {args.bridge}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link add name {args.bridge} type bridge'), False
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
pw_prompted = True
if args.bridge_mac:
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.bridge} address {args.bridge_mac}'), False
print(gray(f'Setting bridge {args.bridge} MAC address to {args.bridge_mac}'))
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
pw_prompted = True
if args.bridge_master and get_master(args.bridge) != args.bridge_master:
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.bridge_master} master {args.bridge}'), False
print(gray(f'Setting interface {args.bridge_master} master to {args.bridge}'))
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
pw_prompted = True
print(gray(f'Setting interface {args.tap} master to {args.bridge}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.tap} master {args.bridge}'), False
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
pw_prompted = True
print(gray(f'Bringing up bridge {args.bridge}'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.bridge} up'), False
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
pw_prompted = True
print(gray(f'Bringing interface {args.tap} up'))
handle, pw_prompted = SysCommandWorker(f'sudo ip link set dev {args.tap} up'), False
while handle.is_alive():
if b'password for' in handle and pw_prompted is False:
ensure_sudo()
handle.write(bytes(sudo_password, 'UTF-8'))
pw_prompted = True
def setup_disks():
if args.harddrive:
for harddrive_arg in args.harddrive:
path, size = harddrive_arg.split(':')
path = pathlib.Path(path.strip()).expanduser().resolve().absolute()
harddrives[path] = size.strip()
if path.exists() is False:
handle = SysCommandWorker(f'qemu-img create -f qcow2 {hdd} {size}')
while handle.is_alive():
time.sleep(0.01)
if handle.exit_code != 0:
raise ValueError(f'Could not create harddrive {hdd}: {handle}')
setup_networking()
setup_disks()
if args.uki or args.bios is False:
disk_paths_hash = hashlib.sha1((''.join(sorted([str(x) for x in harddrives.keys()]))).encode()).hexdigest()
shutil.copy2('/usr/share/ovmf/x64/OVMF_CODE.secboot.4m.fd', f'./OVMF_CODE.secboot.4m.fd.{disk_paths_hash}')
shutil.copy2('/usr/share/ovmf/x64/OVMF_VARS.4m.fd', f'./OVMF_VARS.4m.fd.{disk_paths_hash}')
boot_index = 0
qemu = 'qemu-system-x86_64'
qemu += ' -cpu host'
qemu += ' -enable-kvm'
qemu += ' -machine q35,accel=kvm'
qemu += ' -object rng-random,filename=/dev/urandom,id=rng0'
qemu += ' -device virtio-rng-pci,rng=rng0'
qemu += ' -global driver=cfi.pflash01,property=secure,value=on'
qemu += f' -smp {args.cpu},sockets=1,dies=1,cores={args.cpu},threads=1'
# qemu += f' -vga vga'
qemu += f' -device VGA,edid=on,xres={args.resolution.split("x")[0]},yres={args.resolution.split("x")[1]}'
qemu += ' -device intel-iommu,device-iotlb=on,caching-mode=on'
qemu += f' -m {args.memory}'
if args.bios is False:
qemu += f' -drive if=pflash,format=raw,readonly=on,file=./OVMF_CODE.secboot.4m.fd.{disk_paths_hash}'
qemu += f' -drive if=pflash,format=raw,file=./OVMF_VARS.4m.fd.{disk_paths_hash}'
if args.uki:
qemu += f' -kernel {args.uki}'
boot_index += 1
scsi_index = 0
for scsi_index, hdd in enumerate(harddrives.keys()):
# qemu += f' -device virtio-scsi-pci,bus=pcie.0,id=scsi{index}'
# qemu += f' -device scsi-hd,drive=hdd{index},bus=scsi{index}.0,id=scsi{index}.0,bootindex={hdd_boot_priority+index}'
# qemu += f' -drive file={hdd},if=none,format=qcow2,discard=unmap,aio=native,cache=none,id=hdd{index}'
qemu += f' -device virtio-scsi-pci,bus=pcie.0,id=scsi{scsi_index},addr=0x{scsi_index + 8}'
qemu += f' -device scsi-hd,drive=libvirt-{scsi_index}-format,bus=scsi{scsi_index}.0,id=scsi{scsi_index}-0-0-0,channel=0,scsi-id=0,lun=0,device_id=drive-scsi0-0-0-0,bootindex={boot_index},write-cache=on' # noqa: E501
qemu += f' -blockdev \'{{"driver":"file","filename":"{hdd}","aio":"threads","node-name":"libvirt-{scsi_index}-storage","cache":{{"direct":false,"no-flush":false}},"auto-read-only":true,"discard":"unmap"}}\'' # noqa: E501
qemu += f' -blockdev \'{{"node-name":"libvirt-{scsi_index}-format","read-only":false,"discard":"unmap","cache":{{"direct":true,"no-flush":false}},"driver":"qcow2","file":"libvirt-{scsi_index}-storage","backing":null}}\'' # noqa: E501
boot_index += 1
if args.iso:
qemu += f' -device virtio-scsi-pci,bus=pcie.0,id=scsi{scsi_index + 1}'
qemu += f' -device scsi-cd,drive=cdrom0,bus=scsi{scsi_index + 1}.0,bootindex={boot_index}'
qemu += f' -drive file={args.iso},media=cdrom,if=none,format=raw,cache=none,id=cdrom0'
boot_index += 1
# if args.vfio:
# qemu += f' -drive file={args.vfio},index=2,media=cdrom'
if args.tap:
qemu += f' -device virtio-net-pci,mac={args.tap_mac},id=network0,netdev=network0.0,status=on,bus=pcie.0'
qemu += f' -netdev tap,ifname={args.tap},id=network0.0,script=no,downscript=no'
print(gray(qemu))
qemu_session = subprocess.run(shlex.split(qemu), check=True, capture_output=True)
if qemu_session.stdout:
print(qemu_session.stdout.decode())
if qemu_session.returncode != 0:
print(red(qemu_session.stderr.decode()))

View File

@ -1,94 +1,127 @@
# pylint: disable=redefined-outer-name
import string
import urllib.error
from io import BytesIO
from pathlib import Path
from unittest.mock import MagicMock, patch
from unittest.mock import patch
import pytest
from hypothesis import HealthCheck, given, settings
from hypothesis import strategies as st
from archinstall.lib.output import share_install_log
from archinstall.lib.log import share_install_log
urls = st.builds(
'{}://{}.{}/{}'.format,
st.sampled_from(['http', 'https']),
st.text(alphabet=string.ascii_lowercase, min_size=3, max_size=10),
st.sampled_from(['com', 'net', 'org', 'rs']),
st.text(alphabet=string.ascii_lowercase + string.digits, min_size=0, max_size=8),
)
max_bytes = st.one_of(st.none(), st.integers(min_value=1, max_value=130))
random_paths = st.lists(
st.text(
alphabet=string.ascii_lowercase + string.digits,
min_size=1,
max_size=10,
),
min_size=1,
max_size=5,
).map(lambda parts: Path(*parts))
@pytest.fixture()
@pytest.fixture
def log_file(tmp_path: Path) -> Path:
log_dir = tmp_path / 'archinstall'
log_dir.mkdir()
return log_dir / 'install.log'
def _fake_logger(log_file: Path) -> MagicMock:
mock = MagicMock()
mock.path = log_file
return mock
# def _fake_logger(log_file: Path) -> MagicMock:
# mock = MagicMock()
# mock.path = log_file
# return mock
def test_file_not_found(tmp_path: Path) -> None:
missing = tmp_path / 'no-such' / 'install.log'
with patch('archinstall.lib.output.logger', _fake_logger(missing)):
assert share_install_log() == 1
@given(paste_url=urls, max_byte=max_bytes, sub_path=random_paths)
@settings(max_examples=3, suppress_health_check=[HealthCheck.function_scoped_fixture])
def test_file_not_found(
tmp_path: Path,
sub_path: Path,
paste_url: str,
max_byte: int | None,
) -> None:
missing_log = tmp_path / sub_path / 'install.log'
with patch('archinstall.lib.log.logger._path', new=missing_log):
assert share_install_log(paste_url, max_byte) is None
def test_empty_file(log_file: Path) -> None:
@given(paste_url=urls, max_byte=max_bytes)
@settings(max_examples=3, suppress_health_check=[HealthCheck.function_scoped_fixture])
def test_empty_file(log_file: Path, paste_url: str, max_byte: int | None) -> None:
log_file.write_bytes(b'')
with patch('archinstall.lib.output.logger', _fake_logger(log_file)):
assert share_install_log() == 1
with patch('archinstall.lib.log.logger._path', new=log_file.parent):
# with patch('archinstall.lib.log.logger', _fake_logger(log_file)):
assert share_install_log(paste_url, max_byte) is None
def test_user_cancels(log_file: Path) -> None:
@given(paste_url=urls, resp_url=urls, max_byte=max_bytes)
@settings(max_examples=3, suppress_health_check=[HealthCheck.function_scoped_fixture])
def test_successful_upload(log_file: Path, resp_url: str, paste_url: str, max_byte: int | None) -> None:
log_file.write_text('some log content')
with patch('archinstall.lib.output.logger', _fake_logger(log_file)):
assert share_install_log(confirm=lambda _: False) == 1
def test_successful_upload(log_file: Path) -> None:
log_file.write_text('some log content')
fake_response = BytesIO(b'https://paste.rs/abc.def')
fake_response = BytesIO(resp_url.encode())
with (
patch('archinstall.lib.output.logger', _fake_logger(log_file)),
patch('urllib.request.urlopen', return_value=fake_response) as mock_open,
patch('archinstall.lib.log.logger._path', new=log_file.parent),
patch('urllib.request.urlopen', return_value=fake_response),
):
result = share_install_log()
assert result == 0
req = mock_open.call_args[0][0]
assert req.data == b'some log content'
result = share_install_log(paste_url, max_byte)
assert result == resp_url
def test_truncation(log_file: Path) -> None:
max_size = 100
@given(paste_url=urls, resp_url=urls, max_byte=max_bytes)
@settings(max_examples=3, suppress_health_check=[HealthCheck.function_scoped_fixture])
def test_truncation(log_file: Path, resp_url: str, paste_url: str, max_byte: int | None) -> None:
content = b'A' * 50 + b'B' * 80
log_file.write_bytes(content)
fake_response = BytesIO(b'https://paste.rs/abc.def')
fake_response = BytesIO(resp_url.encode())
exptected_byte = len(content) if max_byte is None else max_byte
with (
patch('archinstall.lib.output.logger', _fake_logger(log_file)),
patch('archinstall.lib.log.logger._path', new=log_file.parent),
patch('urllib.request.urlopen', return_value=fake_response) as mock_open,
):
result = share_install_log(max_size=max_size)
assert result == 0
req = mock_open.call_args[0][0]
assert len(req.data) == max_size
assert req.data == content[-max_size:]
_ = share_install_log(paste_url, max_byte)
req = mock_open.call_args[0][0]
assert len(req.data) == exptected_byte
assert req.data == content[-exptected_byte:]
def test_network_error(log_file: Path) -> None:
@given(paste_url=urls, max_byte=max_bytes)
@settings(max_examples=3, suppress_health_check=[HealthCheck.function_scoped_fixture])
def test_network_error(log_file: Path, paste_url: str, max_byte: int | None) -> None:
log_file.write_text('some log content')
with (
patch('archinstall.lib.output.logger', _fake_logger(log_file)),
patch('archinstall.lib.log.logger._path', new=log_file.parent),
patch('urllib.request.urlopen', side_effect=urllib.error.URLError('no network')),
):
assert share_install_log() == 1
assert share_install_log(paste_url, max_byte) is None
def test_unexpected_response(log_file: Path) -> None:
@given(paste_url=urls, max_byte=max_bytes)
@settings(max_examples=3, suppress_health_check=[HealthCheck.function_scoped_fixture])
def test_unexpected_response(log_file: Path, paste_url: str, max_byte: int | None) -> None:
log_file.write_text('some log content')
fake_response = BytesIO(b'ERROR: something went wrong')
with (
patch('archinstall.lib.output.logger', _fake_logger(log_file)),
patch('archinstall.lib.log.logger._path', new=log_file.parent),
patch('urllib.request.urlopen', return_value=fake_response),
):
assert share_install_log() == 1
assert share_install_log(paste_url, max_byte) is None