Compare commits
493 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
b96afcc579 | |
|
|
3bc248b47f | |
|
|
af2120c0e9 | |
|
|
d92a98d3bf | |
|
|
af58671e88 | |
|
|
e17e036f54 | |
|
|
ac4bc442de | |
|
|
5fd2f9b627 | |
|
|
dccd0c8ccf | |
|
|
22bf6e3c35 | |
|
|
b5d323762b | |
|
|
42d9113611 | |
|
|
516a61d8af | |
|
|
e48ca45b0b | |
|
|
74a1066661 | |
|
|
b81fe955f0 | |
|
|
13944f3cca | |
|
|
b95321d38c | |
|
|
3dec5025c3 | |
|
|
f7a6f70fc8 | |
|
|
7fc33c2507 | |
|
|
2de7254b21 | |
|
|
af106eb238 | |
|
|
4265be6e43 | |
|
|
b0b7983af2 | |
|
|
ef1cfe7b56 | |
|
|
7b5dddf34b | |
|
|
dcc38fe9aa | |
|
|
11607851c4 | |
|
|
ba7dbeadfc | |
|
|
b5501d9507 | |
|
|
528c27fbe1 | |
|
|
b18cc57216 | |
|
|
dd34954011 | |
|
|
76629ecc15 | |
|
|
dc7d9cf0a2 | |
|
|
b936fa11e3 | |
|
|
6fefa3b6f5 | |
|
|
3b026cb603 | |
|
|
05560d29af | |
|
|
3223ae4212 | |
|
|
468e33d54f | |
|
|
ccbc53d576 | |
|
|
c4d68855cb | |
|
|
8a929f603f | |
|
|
e1efa34d8a | |
|
|
cb0f3a6eba | |
|
|
1b7a32a3b3 | |
|
|
08cba236f6 | |
|
|
8cc35f41d8 | |
|
|
9010ccf9eb | |
|
|
074dfbb178 | |
|
|
008d303aaf | |
|
|
3795bfc21a | |
|
|
210329ed80 | |
|
|
9e05260df5 | |
|
|
d836ab0a66 | |
|
|
3c4c87bdd6 | |
|
|
efda78cad5 | |
|
|
c8e5c85e20 | |
|
|
bb46e295d1 | |
|
|
de43019094 | |
|
|
6e222bcdd8 | |
|
|
6652197e35 | |
|
|
80ae85cc8b | |
|
|
00834f9c6e | |
|
|
e7d38d0e82 | |
|
|
d1765323d8 | |
|
|
094798b496 | |
|
|
fa54c9d6c9 | |
|
|
b842e54091 | |
|
|
03b245a77a | |
|
|
7d10f9e08b | |
|
|
d80bdb4fad | |
|
|
68d137cdce | |
|
|
82a46c66ca | |
|
|
86dc1bbc12 | |
|
|
cd62eff4a7 | |
|
|
e8ea33c41c | |
|
|
4fcef35af0 | |
|
|
9fdd7eb12e | |
|
|
8fe8d4e35f | |
|
|
02faf9fbb9 | |
|
|
9b9f9d6ac0 | |
|
|
98d72a6b57 | |
|
|
06488dfb9a | |
|
|
29588fadee | |
|
|
3cabe860ae | |
|
|
7ea171369a | |
|
|
8b3be842ca | |
|
|
30f50d9719 | |
|
|
c1771cf615 | |
|
|
c0d3c14b6e | |
|
|
113cc7fd94 | |
|
|
88366ba575 | |
|
|
728bb450c7 | |
|
|
2b27e565ae | |
|
|
a52bfc3446 | |
|
|
5dd94e80d2 | |
|
|
335a7b792c | |
|
|
5553cb9eae | |
|
|
80da3f14a1 | |
|
|
bf9b9cb7c1 | |
|
|
9a2d546882 | |
|
|
d6de03ab76 | |
|
|
51600ecd2a | |
|
|
60bcded743 | |
|
|
934407414e | |
|
|
1a4eedf868 | |
|
|
b2f413124b | |
|
|
101f647319 | |
|
|
9059a18dfe | |
|
|
d6987b4e9d | |
|
|
1bf87c6f4a | |
|
|
6505e17f34 | |
|
|
78987d75fe | |
|
|
b906a34a5d | |
|
|
18c77b94f2 | |
|
|
3ef0848ffe | |
|
|
a92cd50aec | |
|
|
eda9ee338b | |
|
|
caf285a6dc | |
|
|
7609a1634e | |
|
|
f37ae8b282 | |
|
|
d57709cb2f | |
|
|
9e8cbd0181 | |
|
|
3ba29f1193 | |
|
|
a8d265184c | |
|
|
37159dcb6d | |
|
|
e2bd7b3405 | |
|
|
56bec01979 | |
|
|
41858db832 | |
|
|
5a4b6131c1 | |
|
|
c91e19e534 | |
|
|
6c6c8d8000 | |
|
|
6c9f66265a | |
|
|
5544abd380 | |
|
|
deb8080322 | |
|
|
d70e03fa3b | |
|
|
29b73302ae | |
|
|
a3c6bd5d45 | |
|
|
0175949cab | |
|
|
179d9c7b48 | |
|
|
bdf6f92991 | |
|
|
ef4d7cfc3a | |
|
|
603c432e18 | |
|
|
e3e9563aa6 | |
|
|
398d6cfdbe | |
|
|
c76fd90074 | |
|
|
4b9087e2f4 | |
|
|
dc64e15327 | |
|
|
dd0da34ed9 | |
|
|
e763c1109c | |
|
|
1764b4971d | |
|
|
8d6c56ca2b | |
|
|
e06dd6299f | |
|
|
8e34303175 | |
|
|
cfdcebcacd | |
|
|
b21ac742c2 | |
|
|
594cf54c8a | |
|
|
e74a72f482 | |
|
|
b186fb3f64 | |
|
|
ffa130fe99 | |
|
|
e718a12b42 | |
|
|
28f4904a3c | |
|
|
bace59aef8 | |
|
|
8b65bd9a6a | |
|
|
a4bfd379f5 | |
|
|
2b63d573af | |
|
|
33b0131e4e | |
|
|
8638726e7a | |
|
|
6d87a062fd | |
|
|
e133cb0d28 | |
|
|
452abe4277 | |
|
|
813b9b34b3 | |
|
|
64567a748a | |
|
|
b809c84ad5 | |
|
|
501759d979 | |
|
|
839e6bfc55 | |
|
|
74a230dae9 | |
|
|
f19f8d195f | |
|
|
76d6f08841 | |
|
|
390b4959bd | |
|
|
d413c01fde | |
|
|
d781c9d857 | |
|
|
33f3709c31 | |
|
|
ec8bd01ced | |
|
|
3cd85c023e | |
|
|
b6ce255b36 | |
|
|
e4faba2de0 | |
|
|
c92a457142 | |
|
|
01d3af9fbb | |
|
|
683996c44f | |
|
|
4f87ccba28 | |
|
|
3447d2f47d | |
|
|
d86daa531a | |
|
|
a9c939cb86 | |
|
|
415fefef26 | |
|
|
2d18b55fa7 | |
|
|
bb76f96bd5 | |
|
|
377b3bee97 | |
|
|
618c0bd5dc | |
|
|
bd35473b5d | |
|
|
f2c17c6341 | |
|
|
f736b8c4ec | |
|
|
8edab9fafd | |
|
|
083a73eab1 | |
|
|
35c2ff3ef5 | |
|
|
8148b1d9bf | |
|
|
3b6f7db942 | |
|
|
9bd2131792 | |
|
|
ec9ee80062 | |
|
|
cfe61c0993 | |
|
|
6ce0556c2c | |
|
|
a61d4e5314 | |
|
|
21358f77dd | |
|
|
a7901d172d | |
|
|
c0d47db75d | |
|
|
66e0dd119f | |
|
|
13b7c7194a | |
|
|
3dab440cbf | |
|
|
1d8352b466 | |
|
|
0dddc7308d | |
|
|
4a8604ac88 | |
|
|
aac6c896d9 | |
|
|
f8304b0bd1 | |
|
|
0394a735a7 | |
|
|
12bd83ca8a | |
|
|
b5f710425f | |
|
|
331634fd80 | |
|
|
173c64c847 | |
|
|
47a4caf163 | |
|
|
449c1bfb36 | |
|
|
e2ba6cbc54 | |
|
|
97dd099c0e | |
|
|
26014aa092 | |
|
|
6d89770e7a | |
|
|
2cb81d8758 | |
|
|
5612325dc3 | |
|
|
8f104bc829 | |
|
|
18105fff22 | |
|
|
76284b601b | |
|
|
8ffc20b6c1 | |
|
|
bde544caca | |
|
|
316251f6e0 | |
|
|
5fe42bd2f5 | |
|
|
15460e4a88 | |
|
|
d7bcd431a7 | |
|
|
ef3b6ab853 | |
|
|
2421e538a8 | |
|
|
28f7aec2af | |
|
|
a025c79ac3 | |
|
|
93736349a2 | |
|
|
9bd16e998c | |
|
|
7a0b4c2a30 | |
|
|
63e3825756 | |
|
|
a57bb801c0 | |
|
|
2ed6182575 | |
|
|
b07bbeecd3 | |
|
|
2c2b17b8a6 | |
|
|
7fb8fb57ee | |
|
|
3500bc3b41 | |
|
|
192aff0b94 | |
|
|
4d2864ba76 | |
|
|
78969c58c7 | |
|
|
7cf1566902 | |
|
|
c7dbf0105e | |
|
|
38462db2db | |
|
|
0aca992ac5 | |
|
|
9a38b73baf | |
|
|
a150a8d9f7 | |
|
|
7b45613996 | |
|
|
e1d9935f30 | |
|
|
65190a253e | |
|
|
7995d6e815 | |
|
|
2c154245cb | |
|
|
2c85b5eab0 | |
|
|
972e278555 | |
|
|
e5a14c0cfe | |
|
|
c4de093122 | |
|
|
90d1b08628 | |
|
|
20460e1c29 | |
|
|
2e9d5e4829 | |
|
|
5811f81e59 | |
|
|
8f7d59b718 | |
|
|
b779345a5b | |
|
|
f6eca309ac | |
|
|
82512eed0e | |
|
|
db6c11345a | |
|
|
3374b47d50 | |
|
|
bde3b0ed6e | |
|
|
b1290672bb | |
|
|
43509d8ce1 | |
|
|
efd57870d0 | |
|
|
cb4b7e3db0 | |
|
|
01bee60cd1 | |
|
|
e0c1b869a6 | |
|
|
d07a9b4a92 | |
|
|
ef9f704761 | |
|
|
5fbe1deea4 | |
|
|
7292a9e449 | |
|
|
85d38f063b | |
|
|
62edc56a52 | |
|
|
84c36adb1f | |
|
|
e2e158973f | |
|
|
ef3369b1c5 | |
|
|
bb13139339 | |
|
|
0470495cb1 | |
|
|
8e0ff2d2a9 | |
|
|
cec29d123b | |
|
|
f2e40742bb | |
|
|
0188459917 | |
|
|
cc6e247dcf | |
|
|
450664cdc4 | |
|
|
e590277e69 | |
|
|
9488233a5a | |
|
|
146a2352bc | |
|
|
5eb3307efd | |
|
|
d1a74edf9d | |
|
|
fd143af05b | |
|
|
4582d60f13 | |
|
|
7889a5417f | |
|
|
bff715ddab | |
|
|
051352e218 | |
|
|
9433f15573 | |
|
|
d5b554be1e | |
|
|
aabf6ae19e | |
|
|
cb6fe6b34b | |
|
|
446d23c59d | |
|
|
83c9bf06b2 | |
|
|
eb5e88f317 | |
|
|
9f4f29bd29 | |
|
|
2954e4397b | |
|
|
747385a883 | |
|
|
ac984b7622 | |
|
|
4e52dc7493 | |
|
|
79313c4942 | |
|
|
c1eae10e93 | |
|
|
42a4ee8472 | |
|
|
6968a33508 | |
|
|
4d01d1dbbb | |
|
|
7635474772 | |
|
|
a4ad1b3724 | |
|
|
5fcea379b9 | |
|
|
e5ccdb0c1c | |
|
|
9e7a5f6931 | |
|
|
9412f97771 | |
|
|
1faac77c0d | |
|
|
1227babd8c | |
|
|
ba5f924540 | |
|
|
f639290c20 | |
|
|
043cd68a7c | |
|
|
9876558f74 | |
|
|
d7b559c67e | |
|
|
cf31148e46 | |
|
|
dee2a84057 | |
|
|
17dc001857 | |
|
|
6f768ea87c | |
|
|
e2cdf42690 | |
|
|
b579773421 | |
|
|
d13882fca1 | |
|
|
6b23eff422 | |
|
|
810a50e46c | |
|
|
582c54f4e2 | |
|
|
bc738a48be | |
|
|
0a6fd6a07e | |
|
|
20718ead79 | |
|
|
fff783ed93 | |
|
|
f65cc5f6c7 | |
|
|
cffe47a369 | |
|
|
0117205f73 | |
|
|
1f1daf1afc | |
|
|
c9b0b171b3 | |
|
|
326232098a | |
|
|
6b50815eb6 | |
|
|
57bd613679 | |
|
|
8872f98c8d | |
|
|
601e033188 | |
|
|
07ab6bf4a0 | |
|
|
7398e2785d | |
|
|
d176b9514c | |
|
|
66fb49617a | |
|
|
dbfa60bfa8 | |
|
|
e7442bf5c6 | |
|
|
627530fd82 | |
|
|
94f22fcc55 | |
|
|
8387c8f589 | |
|
|
3290175084 | |
|
|
0221099398 | |
|
|
29cb454fda | |
|
|
6fc0bc3671 | |
|
|
7732d50016 | |
|
|
d7e5dc3692 | |
|
|
70a6c3499a | |
|
|
b751ad5dab | |
|
|
147447face | |
|
|
60302060e3 | |
|
|
5f0742a87a | |
|
|
73ceb0c99d | |
|
|
2ee66a059e | |
|
|
a94d94a8e1 | |
|
|
eb815d817f | |
|
|
840c1c0f7e | |
|
|
bf3784fc0c | |
|
|
4c8fe60a28 | |
|
|
46072132a1 | |
|
|
c3aba06a01 | |
|
|
375d64a600 | |
|
|
6bdb756650 | |
|
|
803ff4236e | |
|
|
117b9d9a5d | |
|
|
e6bbd05121 | |
|
|
3da27570f7 | |
|
|
566dcb51c3 | |
|
|
b77f03e94a | |
|
|
811c1d95b5 | |
|
|
165636b2e6 | |
|
|
e7553c114c | |
|
|
b2fccb0b3b | |
|
|
83aab9cd4f | |
|
|
a996670a60 | |
|
|
240919826f | |
|
|
c131269c08 | |
|
|
a3d85c1c58 | |
|
|
bf8f72cc54 | |
|
|
996566291d | |
|
|
2808442f8b | |
|
|
76ab9482e9 | |
|
|
7af94c8fe5 | |
|
|
c2d3daeb25 | |
|
|
1174800cca | |
|
|
adbadbf606 | |
|
|
ed67e9fd67 | |
|
|
bf6245815b | |
|
|
4ceddacb4e | |
|
|
00c90d9e45 | |
|
|
a03e9bcdc6 | |
|
|
552c7b7dd9 | |
|
|
772652bc69 | |
|
|
b0679974d5 | |
|
|
90bf24b3ce | |
|
|
e0ea072032 | |
|
|
1fa8f2013b | |
|
|
6505c073c9 | |
|
|
a84a97fdc7 | |
|
|
0a032b04d5 | |
|
|
46380f5da3 | |
|
|
6c43a8c6d3 | |
|
|
53e8cb3a8c | |
|
|
daab8e6ff6 | |
|
|
8fb83a7beb | |
|
|
b0b87400c2 | |
|
|
97790145c4 | |
|
|
d7f74b4b32 | |
|
|
472ffd4e6a | |
|
|
68d8af3df3 | |
|
|
88266f5c74 | |
|
|
a3e219a2e4 | |
|
|
5777880d18 | |
|
|
c8429aee23 | |
|
|
60e70f507f | |
|
|
b03948bbfe | |
|
|
21f9972040 | |
|
|
e0ce0bdd45 | |
|
|
c86b0be79f | |
|
|
853309d311 | |
|
|
2f1b8cd692 | |
|
|
e1f7a314a7 | |
|
|
ebeb4c7daa | |
|
|
88753b1f40 | |
|
|
ca85f39b71 | |
|
|
5f96390719 | |
|
|
fceee34dfa | |
|
|
49ead6fa38 | |
|
|
792db9e8b8 | |
|
|
df3b5d7593 | |
|
|
d256e70480 | |
|
|
de05dcb7e0 | |
|
|
9ddd41da56 | |
|
|
cec68b6e9b | |
|
|
c60d9bd232 | |
|
|
ad02f5bffc | |
|
|
3c416943e2 | |
|
|
e846a0ec58 | |
|
|
d40d77ce25 | |
|
|
de50a31609 | |
|
|
1ef52f56cb | |
|
|
bca3f4b660 | |
|
|
9bb7ced10e | |
|
|
b8baf096dd | |
|
|
0ad513a8e5 | |
|
|
ed8b6ac045 | |
|
|
fd2c20a900 |
|
|
@ -0,0 +1,4 @@
|
||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [archlinux]
|
||||||
|
custom: ['https://archlinux.org/donate/']
|
||||||
|
|
@ -41,8 +41,8 @@ body:
|
||||||
attributes:
|
attributes:
|
||||||
value: >
|
value: >
|
||||||
**Note**: Assuming you have network connectivity,
|
**Note**: Assuming you have network connectivity,
|
||||||
you can easily post the installation log using the following command:
|
you can easily upload the installation log and get a shareable URL by running:
|
||||||
`curl -F'file=@/var/log/archinstall/install.log' https://0x0.st`
|
`archinstall share-log`
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: freeform
|
id: freeform
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ jobs:
|
||||||
container:
|
container:
|
||||||
image: archlinux/archlinux:latest
|
image: archlinux/archlinux:latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
- run: pacman --noconfirm -Syu bandit
|
- run: pacman --noconfirm -Syu bandit
|
||||||
- name: Security checkup with Bandit
|
- name: Security checkup with Bandit
|
||||||
run: bandit -r archinstall || exit 0
|
run: bandit -r archinstall || exit 0
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ jobs:
|
||||||
container:
|
container:
|
||||||
image: archlinux/archlinux:latest
|
image: archlinux/archlinux:latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
- name: Prepare arch
|
- name: Prepare arch
|
||||||
run: |
|
run: |
|
||||||
pacman-key --init
|
pacman-key --init
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ jobs:
|
||||||
image: archlinux/archlinux:latest
|
image: archlinux/archlinux:latest
|
||||||
options: --privileged
|
options: --privileged
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||||
- name: Install pre-dependencies
|
- name: Install pre-dependencies
|
||||||
run: |
|
run: |
|
||||||
pacman -Sy --noconfirm tree git python-pyparted python-setuptools python-sphinx python-sphinx_rtd_theme python-build python-installer python-wheel
|
pacman -Sy --noconfirm tree git python-pyparted python-setuptools python-sphinx python-sphinx_rtd_theme python-build python-installer python-wheel
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,14 @@ jobs:
|
||||||
image: archlinux/archlinux:latest
|
image: archlinux/archlinux:latest
|
||||||
options: --privileged
|
options: --privileged
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
- run: pwd
|
- run: pwd
|
||||||
- run: find .
|
- run: find .
|
||||||
- run: cat /etc/os-release
|
- run: cat /etc/os-release
|
||||||
- run: pacman-key --init
|
- run: pacman-key --init
|
||||||
- run: pacman --noconfirm -Sy archlinux-keyring
|
- run: pacman --noconfirm -Sy archlinux-keyring
|
||||||
- run: ./build_iso.sh
|
- run: ./test_tooling/mkarchiso/build_iso.sh
|
||||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: Arch Live ISO
|
name: Arch Live ISO
|
||||||
path: /tmp/archlive/out/*.iso
|
path: /tmp/archlive/out/*.iso
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ jobs:
|
||||||
container:
|
container:
|
||||||
image: archlinux/archlinux:latest
|
image: archlinux/archlinux:latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
- name: Prepare arch
|
- name: Prepare arch
|
||||||
run: |
|
run: |
|
||||||
pacman-key --init
|
pacman-key --init
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ jobs:
|
||||||
container:
|
container:
|
||||||
image: archlinux/archlinux:latest
|
image: archlinux/archlinux:latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
- name: Prepare arch
|
- name: Prepare arch
|
||||||
run: |
|
run: |
|
||||||
pacman-key --init
|
pacman-key --init
|
||||||
|
|
@ -14,7 +14,7 @@ jobs:
|
||||||
pacman --noconfirm -Syyu
|
pacman --noconfirm -Syyu
|
||||||
pacman --noconfirm -Sy python-pip python-pyparted pkgconfig gcc
|
pacman --noconfirm -Sy python-pip python-pyparted pkgconfig gcc
|
||||||
- run: pip install --break-system-packages --upgrade pip
|
- run: pip install --break-system-packages --upgrade pip
|
||||||
- name: Install Pylint and Pylint plug-ins
|
- name: Install Pylint
|
||||||
run: pip install --break-system-packages .[dev]
|
run: pip install --break-system-packages .[dev]
|
||||||
- run: python --version
|
- run: python --version
|
||||||
- run: pylint --version
|
- run: pylint --version
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ jobs:
|
||||||
image: archlinux/archlinux:latest
|
image: archlinux/archlinux:latest
|
||||||
options: --privileged
|
options: --privileged
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
- name: Prepare arch
|
- name: Prepare arch
|
||||||
run: |
|
run: |
|
||||||
pacman-key --init
|
pacman-key --init
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,14 @@ jobs:
|
||||||
image: archlinux/archlinux:latest
|
image: archlinux/archlinux:latest
|
||||||
options: --privileged
|
options: --privileged
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
- name: Prepare arch
|
- name: Prepare arch
|
||||||
run: |
|
run: |
|
||||||
pacman-key --init
|
pacman-key --init
|
||||||
pacman --noconfirm -Sy archlinux-keyring
|
pacman --noconfirm -Sy archlinux-keyring
|
||||||
pacman --noconfirm -Syyu
|
pacman --noconfirm -Syyu
|
||||||
pacman --noconfirm -Sy python-uv python-setuptools python-pip
|
pacman --noconfirm -Sy python-uv python-setuptools python-pip
|
||||||
pacman --noconfirm -Sy python-pyparted python-pydantic
|
pacman --noconfirm -Sy python-pyparted python-pydantic python-textual
|
||||||
- name: Remove existing archinstall (if any)
|
- name: Remove existing archinstall (if any)
|
||||||
run:
|
run:
|
||||||
uv pip uninstall archinstall --break-system-packages --system
|
uv pip uninstall archinstall --break-system-packages --system
|
||||||
|
|
@ -33,7 +33,7 @@ jobs:
|
||||||
archinstall --script guided -v
|
archinstall --script guided -v
|
||||||
archinstall --script only_hd -v
|
archinstall --script only_hd -v
|
||||||
archinstall --script minimal -v
|
archinstall --script minimal -v
|
||||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: archinstall
|
name: archinstall
|
||||||
path: dist/*
|
path: dist/*
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,13 @@ jobs:
|
||||||
image: archlinux/archlinux:latest
|
image: archlinux/archlinux:latest
|
||||||
options: --privileged
|
options: --privileged
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
- name: Prepare arch
|
- name: Prepare arch
|
||||||
run: |
|
run: |
|
||||||
pacman-key --init
|
pacman-key --init
|
||||||
pacman --noconfirm -Sy archlinux-keyring
|
pacman --noconfirm -Sy archlinux-keyring
|
||||||
pacman --noconfirm -Syyu
|
pacman --noconfirm -Syyu
|
||||||
pacman --noconfirm -Sy python python-uv python-setuptools python-pip python-pyparted python-pydantic
|
pacman --noconfirm -Sy python python-uv python-setuptools python-pip python-pyparted python-pydantic python-textual
|
||||||
- name: Build archinstall
|
- name: Build archinstall
|
||||||
run: |
|
run: |
|
||||||
uv build --no-build-isolation --wheel
|
uv build --no-build-isolation --wheel
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,6 @@ jobs:
|
||||||
ruff_format_check:
|
ruff_format_check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
- uses: astral-sh/ruff-action@57714a7c8a2e59f32539362ba31877a1957dded1 # v3.5.1
|
- uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0
|
||||||
- run: ruff format --diff
|
- run: ruff format --diff
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,5 @@ jobs:
|
||||||
ruff:
|
ruff:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
- uses: astral-sh/ruff-action@57714a7c8a2e59f32539362ba31877a1957dded1 # v3.5.1
|
- uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,22 @@
|
||||||
#on:
|
name: Translation validation
|
||||||
# push:
|
on:
|
||||||
# paths:
|
push:
|
||||||
# - 'archinstall/locales/**'
|
paths:
|
||||||
# pull_request:
|
- 'archinstall/**/*.py'
|
||||||
# paths:
|
- 'archinstall/locales/**'
|
||||||
# - 'archinstall/locales/**'
|
- '.github/workflows/translation-check.yaml'
|
||||||
#name: Verify local_generate script was run on translation changes
|
pull_request:
|
||||||
#jobs:
|
paths:
|
||||||
# translation-check:
|
- 'archinstall/**/*.py'
|
||||||
# runs-on: ubuntu-latest
|
- 'archinstall/locales/**'
|
||||||
# container:
|
- '.github/workflows/translation-check.yaml'
|
||||||
# image: archlinux/archlinux:latest
|
jobs:
|
||||||
# steps:
|
translations:
|
||||||
# - uses: actions/checkout@v4
|
name: Validate translations
|
||||||
# - run: pacman --noconfirm -Syu python git diffutils
|
runs-on: ubuntu-latest
|
||||||
# - name: Verify all translation scripts are up to date
|
steps:
|
||||||
# run: |
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
# cd ..
|
- name: Install gettext
|
||||||
# cp -r archinstall archinstall_orig
|
run: sudo apt-get update && sudo apt-get install -y gettext
|
||||||
# cd archinstall/archinstall/locales
|
- name: Run translation checks
|
||||||
# bash locales_generator.sh 1> /dev/null
|
run: bash archinstall/locales/locales_generator.sh check
|
||||||
# cd ../../..
|
|
||||||
# git diff \
|
|
||||||
# --quiet --no-index --name-only \
|
|
||||||
# archinstall_orig/archinstall/locales \
|
|
||||||
# archinstall/archinstall/locales \
|
|
||||||
# || (echo "Translation files have not been updated after translation, please run ./locales_generator.sh once more and commit" && exit 1)
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -39,4 +39,9 @@ requirements.txt
|
||||||
/.gitconfig
|
/.gitconfig
|
||||||
/actions-runner
|
/actions-runner
|
||||||
/cmd_output.txt
|
/cmd_output.txt
|
||||||
|
node_modules/
|
||||||
uv.lock
|
uv.lock
|
||||||
|
test_tooling/mkosi/mkosi.output/*image*
|
||||||
|
test_tooling/mkosi/mkosi.cache/**
|
||||||
|
test_tooling/mkosi/mkosi.tools/**
|
||||||
|
test_tooling/mkosi/mkosi.tools.manifest
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ flake8:
|
||||||
- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||||
|
|
||||||
# We currently do not have unit tests implemented but this stage is written in anticipation of their future usage.
|
# We currently do not have unit tests implemented but this stage is written in anticipation of their future usage.
|
||||||
# When a stage name is preceeded with a '.' it's treated as "disabled" by GitLab and is not executed, so it's fine for it to be declared.
|
# When a stage name is preceded with a '.' it's treated as "disabled" by GitLab and is not executed, so it's fine for it to be declared.
|
||||||
.pytest:
|
.pytest:
|
||||||
stage: test
|
stage: test
|
||||||
tags:
|
tags:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
default_stages: ['pre-commit']
|
default_stages: ['pre-commit']
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.12.11
|
rev: v0.15.14
|
||||||
hooks:
|
hooks:
|
||||||
# fix unused imports and sort them
|
# fix unused imports and sort them
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
|
@ -31,7 +31,7 @@ repos:
|
||||||
args: [--config=.flake8]
|
args: [--config=.flake8]
|
||||||
fail_fast: true
|
fail_fast: true
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v1.17.1
|
rev: v2.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
args: [
|
args: [
|
||||||
|
|
@ -41,7 +41,9 @@ repos:
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- pydantic
|
- pydantic
|
||||||
- pytest
|
- pytest
|
||||||
|
- hypothesis
|
||||||
- cryptography
|
- cryptography
|
||||||
|
- textual
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: pylint
|
- id: pylint
|
||||||
|
|
|
||||||
7
PKGBUILD
7
PKGBUILD
|
|
@ -5,7 +5,7 @@
|
||||||
# Contributor: demostanis worlds <demostanis@protonmail.com>
|
# Contributor: demostanis worlds <demostanis@protonmail.com>
|
||||||
|
|
||||||
pkgname=archinstall
|
pkgname=archinstall
|
||||||
pkgver=3.0.10
|
pkgver=4.3
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Just another guided/automated Arch Linux installer with a twist"
|
pkgdesc="Just another guided/automated Arch Linux installer with a twist"
|
||||||
arch=(any)
|
arch=(any)
|
||||||
|
|
@ -28,12 +28,15 @@ depends=(
|
||||||
'python-cryptography'
|
'python-cryptography'
|
||||||
'python-pydantic'
|
'python-pydantic'
|
||||||
'python-pyparted'
|
'python-pyparted'
|
||||||
|
'python-textual'
|
||||||
|
'python-markdown-it-py'
|
||||||
|
'python-linkify-it-py'
|
||||||
'systemd'
|
'systemd'
|
||||||
'util-linux'
|
'util-linux'
|
||||||
'xfsprogs'
|
'xfsprogs'
|
||||||
'lvm2'
|
'lvm2'
|
||||||
'f2fs-tools'
|
'f2fs-tools'
|
||||||
'ntfs-3g'
|
'libfido2'
|
||||||
)
|
)
|
||||||
makedepends=(
|
makedepends=(
|
||||||
'python-build'
|
'python-build'
|
||||||
|
|
|
||||||
103
README.md
103
README.md
|
|
@ -6,7 +6,7 @@
|
||||||
[](https://github.com/archlinux/archinstall/actions/workflows/flake8.yaml)
|
[](https://github.com/archlinux/archinstall/actions/workflows/flake8.yaml)
|
||||||
|
|
||||||
Just another guided/automated [Arch Linux](https://wiki.archlinux.org/index.php/Arch_Linux) installer with a twist.
|
Just another guided/automated [Arch Linux](https://wiki.archlinux.org/index.php/Arch_Linux) installer with a twist.
|
||||||
The installer also doubles as a python library to install Arch Linux and manage services, packages, and other things inside the installed system *(Usually from a live medium)*.
|
The installer also doubles as a python library to install Arch Linux and manage services, packages, and other things inside the installed system *(Usually from a live medium or from an existing installation)*.
|
||||||
|
|
||||||
* archinstall [discord](https://discord.gg/aDeMffrxNg) server
|
* archinstall [discord](https://discord.gg/aDeMffrxNg) server
|
||||||
* archinstall [#archinstall:matrix.org](https://matrix.to/#/#archinstall:matrix.org) Matrix channel
|
* archinstall [#archinstall:matrix.org](https://matrix.to/#/#archinstall:matrix.org) Matrix channel
|
||||||
|
|
@ -14,28 +14,53 @@ The installer also doubles as a python library to install Arch Linux and manage
|
||||||
* archinstall [documentation](https://archinstall.archlinux.page/)
|
* archinstall [documentation](https://archinstall.archlinux.page/)
|
||||||
|
|
||||||
# Installation & Usage
|
# Installation & Usage
|
||||||
|
> [!TIP]
|
||||||
|
> In the ISO you are root by default. Use sudo if running from an existing system.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo pacman -S archinstall
|
pacman-key --init
|
||||||
```
|
pacman -Sy archinstall
|
||||||
|
|
||||||
Alternative ways to install are `git clone` the repository or `pip install --upgrade archinstall`.
|
|
||||||
|
|
||||||
## Running the [guided](https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py) installer
|
|
||||||
|
|
||||||
Assuming you are on an Arch Linux live-ISO or installed via `pip`:
|
|
||||||
```shell
|
|
||||||
archinstall
|
archinstall
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running the [guided](https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py) installer using `git`
|
Alternative ways to install are `git clone` the repository (and is better since you get the latest code regardless of [build date](https://archlinux.org/packages/?sort=&q=archinstall)) or `pip install --upgrade archinstall`.
|
||||||
|
|
||||||
|
## Upgrade `archinstall` on live Arch ISO image
|
||||||
|
|
||||||
|
Upgrading archinstall on the ISO needs to be done via a full system upgrade using
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# cd archinstall-git
|
pacman -Syu
|
||||||
# python -m archinstall
|
```
|
||||||
|
|
||||||
|
When booting from a live USB, the space on the ramdisk is limited and may not be sufficient to allow running a re-installation or upgrade of the installer.
|
||||||
|
In case one runs into this issue, any of the following can be used
|
||||||
|
|
||||||
|
* Resize the root partition https://wiki.archlinux.org/title/Archiso#Adjusting_the_size_of_the_root_file_system
|
||||||
|
* Specify the boot parameter copytoram=y (https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio-archiso/-/blob/master/docs/README.bootparams#L26) which will copy the root filesystem to tmpfs
|
||||||
|
|
||||||
|
## Running the [guided](https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py) installer
|
||||||
|
|
||||||
|
Assuming you are on an Arch Linux live-ISO or installed via `pip`, `archinstall` will use the `guided` script by default
|
||||||
|
```shell
|
||||||
|
archinstall
|
||||||
|
```
|
||||||
|
similar goes for running the [guided](https://github.com/archlinux/archinstall/blob/master/archinstall/scripts/guided.py) installer using `git
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/archlinux/archinstall
|
||||||
|
cd archinstall
|
||||||
|
python -m archinstall $@
|
||||||
|
```
|
||||||
|
|
||||||
|
To run alternative scripts using the `--script` parameter
|
||||||
|
|
||||||
|
```
|
||||||
|
archinstall --script <name>
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Advanced
|
#### Advanced
|
||||||
Some additional options that most users do not need are hidden behind the `--advanced` flag.
|
Some additional options that most users do not need are hidden behind the `--advanced` flag and all options/args can be consulted through `-h` or `--help`.
|
||||||
|
|
||||||
## Running from a declarative configuration file or URL
|
## Running from a declarative configuration file or URL
|
||||||
|
|
||||||
|
|
@ -57,7 +82,7 @@ archinstall --config <path to user config file or URL> --creds <path to user cre
|
||||||
```
|
```
|
||||||
|
|
||||||
### Credentials configuration file encryption
|
### Credentials configuration file encryption
|
||||||
By default all user account credentials are hashed with `yescrypt` and only the hash is stored in the saved `user_credentials.json` file.
|
By default, all user account credentials are hashed with `yescrypt` and only the hash is stored in the saved `user_credentials.json` file.
|
||||||
This is not possible for disk encryption password which needs to be stored in plaintext to be able to apply it.
|
This is not possible for disk encryption password which needs to be stored in plaintext to be able to apply it.
|
||||||
|
|
||||||
However, when selecting to save configuration files, `archinstall` will prompt for the option to encrypt the `user_credentials.json` file content.
|
However, when selecting to save configuration files, `archinstall` will prompt for the option to encrypt the `user_credentials.json` file content.
|
||||||
|
|
@ -70,15 +95,15 @@ there are multiple ways to provide the decryption key:
|
||||||
|
|
||||||
# Help or Issues
|
# Help or Issues
|
||||||
|
|
||||||
If you come across any issues, kindly submit your issue here on Github or post your query in the
|
If you come across any issues, kindly submit your issue here on GitHub or post your query in the
|
||||||
[discord](https://discord.gg/aDeMffrxNg) help channel.
|
[discord](https://discord.gg/aDeMffrxNg) help channel.
|
||||||
|
|
||||||
When submitting an issue, please:
|
When submitting an issue, please:
|
||||||
* Provide the stacktrace of the output if applicable
|
* Provide the stacktrace of the output if applicable
|
||||||
* Attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you!
|
* Attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you!
|
||||||
* To extract the log from the ISO image, one way is to use<br>
|
* To upload the log from the ISO image and get a shareable URL, run<br>
|
||||||
```shell
|
```shell
|
||||||
curl -F'file=@/var/log/archinstall/install.log' https://0x0.st
|
archinstall share-log
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -133,12 +158,6 @@ The profiles' definitions and the packages they will install can be directly vie
|
||||||
If you want to test a commit, branch, or bleeding edge release from the repository using the standard Arch Linux Live ISO image,
|
If you want to test a commit, branch, or bleeding edge release from the repository using the standard Arch Linux Live ISO image,
|
||||||
replace the archinstall version with a newer one and execute the subsequent steps defined below.
|
replace the archinstall version with a newer one and execute the subsequent steps defined below.
|
||||||
|
|
||||||
*Note: When booting from a live USB, the space on the ramdisk is limited and may not be sufficient to allow
|
|
||||||
running a re-installation or upgrade of the installer. In case one runs into this issue, any of the following can be used
|
|
||||||
- Resize the root partition https://wiki.archlinux.org/title/Archiso#Adjusting_the_size_of_the_root_file_system
|
|
||||||
- The boot parameter `copytoram=y` (https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio-archiso/-/blob/master/docs/README.bootparams#L26)
|
|
||||||
can be specified which will copy the root filesystem to tmpfs.*
|
|
||||||
|
|
||||||
1. You need a working network connection
|
1. You need a working network connection
|
||||||
2. Install the build requirements with `pacman -Sy; pacman -S git python-pip gcc pkgconf`
|
2. Install the build requirements with `pacman -Sy; pacman -S git python-pip gcc pkgconf`
|
||||||
*(note that this may or may not work depending on your RAM and current state of the squashfs maximum filesystem free space)*
|
*(note that this may or may not work depending on your RAM and current state of the squashfs maximum filesystem free space)*
|
||||||
|
|
@ -157,10 +176,10 @@ To test this without a live ISO, the simplest approach is to use a local image a
|
||||||
This can be done by installing `pacman -S arch-install-scripts util-linux` locally and doing the following:
|
This can be done by installing `pacman -S arch-install-scripts util-linux` locally and doing the following:
|
||||||
|
|
||||||
# truncate -s 20G testimage.img
|
# truncate -s 20G testimage.img
|
||||||
# losetup --partscan --show --find ./testimage.img
|
# losetup --partscan --show ./testimage.img
|
||||||
# pip install --upgrade archinstall
|
# pip install --upgrade archinstall
|
||||||
# python -m archinstall --script guided
|
# python -m archinstall --script guided
|
||||||
# qemu-system-x86_64 -enable-kvm -machine q35,accel=kvm -device intel-iommu -cpu host -m 4096 -boot order=d -drive file=./testimage.img,format=raw -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF.4m.fd
|
# qemu-system-x86_64 -enable-kvm -machine q35,accel=kvm -device intel-iommu -cpu host -m 4096 -boot order=d -drive file=./testimage.img,format=raw -drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd -drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.4m.fd
|
||||||
|
|
||||||
This will create a *20 GB* `testimage.img` and create a loop device which we can use to format and install to.<br>
|
This will create a *20 GB* `testimage.img` and create a loop device which we can use to format and install to.<br>
|
||||||
`archinstall` is installed and executed in [guided mode](#docs-todo). Once the installation is complete, ~~you can use qemu/kvm to boot the test media.~~<br>
|
`archinstall` is installed and executed in [guided mode](#docs-todo). Once the installation is complete, ~~you can use qemu/kvm to boot the test media.~~<br>
|
||||||
|
|
@ -169,9 +188,41 @@ This will create a *20 GB* `testimage.img` and create a loop device which we can
|
||||||
There's also a [Building and Testing](https://github.com/archlinux/archinstall/wiki/Building-and-Testing) guide.<br>
|
There's also a [Building and Testing](https://github.com/archlinux/archinstall/wiki/Building-and-Testing) guide.<br>
|
||||||
It will go through everything from packaging, building and running *(with qemu)* the installer against a dev branch.
|
It will go through everything from packaging, building and running *(with qemu)* the installer against a dev branch.
|
||||||
|
|
||||||
|
## Boot an Arch ISO image in a VM
|
||||||
|
|
||||||
|
You may want to boot an ISO image in a VM to test `archinstall` in there.
|
||||||
|
|
||||||
|
* Download the latest [Arch ISO](https://archlinux.org/download/)
|
||||||
|
* Use the the below command to boot the ISO in a VM
|
||||||
|
|
||||||
|
```
|
||||||
|
qemu-system-x86_64 -enable-kvm \
|
||||||
|
-machine q35,accel=kvm -device intel-iommu \
|
||||||
|
-cpu host -m 4096 -boot order=d \
|
||||||
|
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
|
||||||
|
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.4m.fd \
|
||||||
|
-drive file=./archlinux-2025.12.01-x86_64.iso,format=raw
|
||||||
|
```
|
||||||
|
|
||||||
|
HINT: For espeakup support
|
||||||
|
```
|
||||||
|
qemu-system-x86_64 -enable-kvm \
|
||||||
|
-machine q35,accel=kvm -device intel-iommu \
|
||||||
|
-cpu host -m 4096 -boot order=d \
|
||||||
|
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
|
||||||
|
-drive if=pflash,format=raw,readonly,file=/usr/share/edk2/x64/OVMF_VARS.4m.fd \
|
||||||
|
-drive file=./archlinux-2025.12.01-x86_64.iso,format=raw \
|
||||||
|
-device intel-hda -device hda-duplex,audiodev=snd0 \
|
||||||
|
-audiodev pa,id=snd0,server=/run/user/1000/pulse/native
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
# FAQ
|
# FAQ
|
||||||
|
|
||||||
|
## AUR
|
||||||
|
|
||||||
|
`archinstall` will not offer or bundle AUR helpers or AUR packages due to a current consensus. This is not any individual developers decision. The reasons and discussions for this stance on the topic can be found on our mailing list thread: [(optional) AUR helper in archinstall](https://lists.archlinux.org/archives/list/arch-dev-public@lists.archlinux.org/thread/VYOULH2GOJLFM2BXOFLWH3D754YXFPSL/).
|
||||||
|
|
||||||
## Keyring out-of-date
|
## Keyring out-of-date
|
||||||
For a description of the problem see https://archinstall.archlinux.page/help/known_issues.html#keyring-is-out-of-date-2213 and discussion in issue https://github.com/archlinux/archinstall/issues/2213.
|
For a description of the problem see https://archinstall.archlinux.page/help/known_issues.html#keyring-is-out-of-date-2213 and discussion in issue https://github.com/archlinux/archinstall/issues/2213.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,143 +1,3 @@
|
||||||
"""Arch Linux installer - guided, templates etc."""
|
from archinstall.lib.plugins import plugin
|
||||||
|
|
||||||
import importlib
|
__all__ = ['plugin']
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from archinstall.lib.args import arch_config_handler
|
|
||||||
from archinstall.lib.disk.utils import disk_layouts
|
|
||||||
from archinstall.lib.packages.packages import check_package_upgrade
|
|
||||||
|
|
||||||
from .lib.hardware import SysInfo
|
|
||||||
from .lib.output import FormattedOutput, debug, error, info, log, warn
|
|
||||||
from .lib.pacman import Pacman
|
|
||||||
from .lib.plugins import load_plugin, plugins
|
|
||||||
from .lib.translationhandler import Language, tr, translation_handler
|
|
||||||
from .tui.curses_menu import Tui
|
|
||||||
|
|
||||||
|
|
||||||
# @archinstall.plugin decorator hook to programmatically add
|
|
||||||
# plugins in runtime. Useful in profiles_bck and other things.
|
|
||||||
def plugin(f, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
|
|
||||||
plugins[f.__name__] = f
|
|
||||||
|
|
||||||
|
|
||||||
def _log_sys_info() -> None:
|
|
||||||
# Log various information about hardware before starting the installation. This might assist in troubleshooting
|
|
||||||
debug(f'Hardware model detected: {SysInfo.sys_vendor()} {SysInfo.product_name()}; UEFI mode: {SysInfo.has_uefi()}')
|
|
||||||
debug(f'Processor model detected: {SysInfo.cpu_model()}')
|
|
||||||
debug(f'Memory statistics: {SysInfo.mem_available()} available out of {SysInfo.mem_total()} total installed')
|
|
||||||
debug(f'Virtualization detected: {SysInfo.virtualization()}; is VM: {SysInfo.is_vm()}')
|
|
||||||
debug(f'Graphics devices detected: {SysInfo._graphics_devices().keys()}')
|
|
||||||
|
|
||||||
# For support reasons, we'll log the disk layout pre installation to match against post-installation layout
|
|
||||||
debug(f'Disk states before installing:\n{disk_layouts()}')
|
|
||||||
|
|
||||||
|
|
||||||
def _fetch_arch_db() -> None:
|
|
||||||
info('Fetching Arch Linux package database...')
|
|
||||||
try:
|
|
||||||
Pacman.run('-Sy')
|
|
||||||
except Exception as e:
|
|
||||||
error('Failed to sync Arch Linux package database.')
|
|
||||||
if 'could not resolve host' in str(e).lower():
|
|
||||||
error('Most likely due to a missing network connection or DNS issue.')
|
|
||||||
error('Run archinstall --debug and check /var/log/archinstall/install.log for details.')
|
|
||||||
|
|
||||||
debug(f'Failed to sync Arch Linux package database: {e}')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def _check_new_version() -> None:
|
|
||||||
info('Checking version...')
|
|
||||||
upgrade = None
|
|
||||||
|
|
||||||
upgrade = check_package_upgrade('archinstall')
|
|
||||||
|
|
||||||
if upgrade is None:
|
|
||||||
debug('No archinstall upgrades found')
|
|
||||||
return None
|
|
||||||
|
|
||||||
text = tr('New version available') + f': {upgrade}'
|
|
||||||
info(text)
|
|
||||||
time.sleep(3)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
"""
|
|
||||||
This can either be run as the compiled and installed application: python setup.py install
|
|
||||||
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 '--help' in sys.argv or '-h' in sys.argv:
|
|
||||||
arch_config_handler.print_help()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if os.getuid() != 0:
|
|
||||||
print(tr('Archinstall requires root privileges to run. See --help for more.'))
|
|
||||||
return 1
|
|
||||||
|
|
||||||
_log_sys_info()
|
|
||||||
|
|
||||||
if not arch_config_handler.args.offline:
|
|
||||||
_fetch_arch_db()
|
|
||||||
|
|
||||||
if not arch_config_handler.args.skip_version_check:
|
|
||||||
_check_new_version()
|
|
||||||
|
|
||||||
script = arch_config_handler.get_script()
|
|
||||||
|
|
||||||
mod_name = f'archinstall.scripts.{script}'
|
|
||||||
# by loading the module we'll automatically run the script
|
|
||||||
importlib.import_module(mod_name)
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def run_as_a_module() -> None:
|
|
||||||
rc = 0
|
|
||||||
exc = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
rc = main()
|
|
||||||
except Exception as e:
|
|
||||||
exc = e
|
|
||||||
finally:
|
|
||||||
# restore the terminal to the original state
|
|
||||||
Tui.shutdown()
|
|
||||||
|
|
||||||
if exc:
|
|
||||||
err = ''.join(traceback.format_exception(exc))
|
|
||||||
error(err)
|
|
||||||
|
|
||||||
text = (
|
|
||||||
'Archinstall experienced the above error. If you think this is a bug, please report it to\n'
|
|
||||||
'https://github.com/archlinux/archinstall and include the log file "/var/log/archinstall/install.log".\n\n'
|
|
||||||
"Hint: To extract the log from a live ISO \ncurl -F'file=@/var/log/archinstall/install.log' https://0x0.st\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
warn(text)
|
|
||||||
rc = 1
|
|
||||||
|
|
||||||
exit(rc)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'FormattedOutput',
|
|
||||||
'Language',
|
|
||||||
'Pacman',
|
|
||||||
'SysInfo',
|
|
||||||
'Tui',
|
|
||||||
'arch_config_handler',
|
|
||||||
'debug',
|
|
||||||
'disk_layouts',
|
|
||||||
'error',
|
|
||||||
'info',
|
|
||||||
'load_plugin',
|
|
||||||
'log',
|
|
||||||
'plugin',
|
|
||||||
'translation_handler',
|
|
||||||
'warn',
|
|
||||||
]
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import archinstall
|
import sys
|
||||||
|
|
||||||
|
from archinstall.main import main
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
archinstall.run_as_a_module()
|
sys.exit(main())
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from archinstall.lib.hardware import SysInfo
|
from archinstall.lib.hardware import SysInfo
|
||||||
|
from archinstall.lib.log import debug
|
||||||
from archinstall.lib.models.application import Audio, AudioConfiguration
|
from archinstall.lib.models.application import Audio, AudioConfiguration
|
||||||
from archinstall.lib.models.users import User
|
from archinstall.lib.models.users import User
|
||||||
from archinstall.lib.output import debug
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from archinstall.lib.installer import Installer
|
from archinstall.lib.installer import Installer
|
||||||
|
|
@ -30,8 +30,8 @@ class AudioApp:
|
||||||
|
|
||||||
def _enable_pipewire(
|
def _enable_pipewire(
|
||||||
self,
|
self,
|
||||||
install_session: 'Installer',
|
install_session: Installer,
|
||||||
users: list['User'] | None = None,
|
users: list[User] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if users is None:
|
if users is None:
|
||||||
return
|
return
|
||||||
|
|
@ -56,7 +56,7 @@ class AudioApp:
|
||||||
|
|
||||||
def install(
|
def install(
|
||||||
self,
|
self,
|
||||||
install_session: 'Installer',
|
install_session: Installer,
|
||||||
audio_config: AudioConfiguration,
|
audio_config: AudioConfiguration,
|
||||||
users: list[User] | None = None,
|
users: list[User] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from archinstall.lib.output import debug
|
from archinstall.lib.log import debug
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from archinstall.lib.installer import Installer
|
from archinstall.lib.installer import Installer
|
||||||
|
|
@ -20,7 +20,7 @@ class BluetoothApp:
|
||||||
'bluetooth.service',
|
'bluetooth.service',
|
||||||
]
|
]
|
||||||
|
|
||||||
def install(self, install_session: 'Installer') -> None:
|
def install(self, install_session: Installer) -> None:
|
||||||
debug('Installing Bluetooth')
|
debug('Installing Bluetooth')
|
||||||
install_session.add_additional_packages(self.packages)
|
install_session.add_additional_packages(self.packages)
|
||||||
install_session.enable_service(self.services)
|
install_session.enable_service(self.services)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from archinstall.lib.log import debug
|
||||||
|
from archinstall.lib.models.application import Firewall, FirewallConfiguration
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from archinstall.lib.installer import Installer
|
||||||
|
|
||||||
|
|
||||||
|
class FirewallApp:
|
||||||
|
@property
|
||||||
|
def ufw_packages(self) -> list[str]:
|
||||||
|
return [
|
||||||
|
'ufw',
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fwd_packages(self) -> list[str]:
|
||||||
|
return [
|
||||||
|
'firewalld',
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ufw_services(self) -> list[str]:
|
||||||
|
return [
|
||||||
|
'ufw.service',
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fwd_services(self) -> list[str]:
|
||||||
|
return [
|
||||||
|
'firewalld.service',
|
||||||
|
]
|
||||||
|
|
||||||
|
def install(
|
||||||
|
self,
|
||||||
|
install_session: Installer,
|
||||||
|
firewall_config: FirewallConfiguration,
|
||||||
|
) -> None:
|
||||||
|
debug(f'Installing firewall: {firewall_config.firewall.value}')
|
||||||
|
|
||||||
|
match firewall_config.firewall:
|
||||||
|
case Firewall.UFW:
|
||||||
|
install_session.add_additional_packages(self.ufw_packages)
|
||||||
|
install_session.enable_service(self.ufw_services)
|
||||||
|
# write default conf file to enabled
|
||||||
|
ufw_conf = install_session.target / 'etc/ufw/ufw.conf'
|
||||||
|
ufw_conf.write_text(ufw_conf.read_text().replace('ENABLED=no', 'ENABLED=yes'))
|
||||||
|
|
||||||
|
case Firewall.FWD:
|
||||||
|
install_session.add_additional_packages(self.fwd_packages)
|
||||||
|
install_session.enable_service(self.fwd_services)
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from archinstall.lib.log import debug
|
||||||
|
from archinstall.lib.models.application import FontsConfiguration
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from archinstall.lib.installer import Installer
|
||||||
|
|
||||||
|
|
||||||
|
class FontsApp:
|
||||||
|
def install(self, install_session: Installer, fonts_config: FontsConfiguration) -> None:
|
||||||
|
packages = [f.value for f in fonts_config.fonts]
|
||||||
|
debug(f'Installing fonts: {packages}')
|
||||||
|
install_session.add_additional_packages(packages)
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from archinstall.lib.log import debug
|
||||||
|
from archinstall.lib.models.application import PowerManagement, PowerManagementConfiguration
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from archinstall.lib.installer import Installer
|
||||||
|
|
||||||
|
|
||||||
|
class PowerManagementApp:
|
||||||
|
@property
|
||||||
|
def ppd_packages(self) -> list[str]:
|
||||||
|
return [
|
||||||
|
'power-profiles-daemon',
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tuned_packages(self) -> list[str]:
|
||||||
|
return [
|
||||||
|
'tuned',
|
||||||
|
'tuned-ppd',
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ppd_services(self) -> list[str]:
|
||||||
|
return [
|
||||||
|
'power-profiles-daemon.service',
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tuned_services(self) -> list[str]:
|
||||||
|
return [
|
||||||
|
'tuned.service',
|
||||||
|
]
|
||||||
|
|
||||||
|
def install(
|
||||||
|
self,
|
||||||
|
install_session: Installer,
|
||||||
|
power_management_config: PowerManagementConfiguration,
|
||||||
|
) -> None:
|
||||||
|
debug(f'Installing power management daemon: {power_management_config.power_management.value}')
|
||||||
|
|
||||||
|
match power_management_config.power_management:
|
||||||
|
case PowerManagement.POWER_PROFILES_DAEMON:
|
||||||
|
install_session.add_additional_packages(self.ppd_packages)
|
||||||
|
install_session.enable_service(self.ppd_services)
|
||||||
|
case PowerManagement.TUNED:
|
||||||
|
install_session.add_additional_packages(self.tuned_packages)
|
||||||
|
install_session.enable_service(self.tuned_services)
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from archinstall.lib.log import debug
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from archinstall.lib.installer import Installer
|
||||||
|
|
||||||
|
|
||||||
|
class PrintServiceApp:
|
||||||
|
@property
|
||||||
|
def packages(self) -> list[str]:
|
||||||
|
return ['cups', 'system-config-printer', 'cups-pk-helper']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def services(self) -> list[str]:
|
||||||
|
return [
|
||||||
|
'cups.service',
|
||||||
|
]
|
||||||
|
|
||||||
|
def install(self, install_session: Installer) -> None:
|
||||||
|
debug('Installing print service')
|
||||||
|
install_session.add_additional_packages(self.packages)
|
||||||
|
install_session.enable_service(self.services)
|
||||||
|
|
@ -1,217 +0,0 @@
|
||||||
# from typing import List, Dict, Optional, TYPE_CHECKING, Any
|
|
||||||
#
|
|
||||||
# from ..lib import menu
|
|
||||||
# from archinstall.lib.output import log, FormattedOutput
|
|
||||||
# from archinstall.lib.profile.profiles_handler import profile_handler
|
|
||||||
# from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult, ProfileInfo, TProfile
|
|
||||||
#
|
|
||||||
# if TYPE_CHECKING:
|
|
||||||
# from archinstall.lib.installer import Installer
|
|
||||||
# _: Any
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# class CustomProfileList(menu.ListManager):
|
|
||||||
# def __init__(self, prompt: str, profiles: List[TProfile]):
|
|
||||||
# self._actions = [
|
|
||||||
# str(_('Add profile')),
|
|
||||||
# str(_('Edit profile')),
|
|
||||||
# str(_('Delete profile'))
|
|
||||||
# ]
|
|
||||||
# super().__init__(prompt, profiles, [self._actions[0]], self._actions[1:])
|
|
||||||
#
|
|
||||||
# def reformat(self, data: List[TProfile]) -> Dict[str, Optional[TProfile]]:
|
|
||||||
# table = FormattedOutput.as_table(data)
|
|
||||||
# rows = table.split('\n')
|
|
||||||
#
|
|
||||||
# # these are the header rows of the table and do not map to any profile obviously
|
|
||||||
# # we're adding 2 spaces as prefix because the menu selector '> ' will be put before
|
|
||||||
# # the selectable rows so the header has to be aligned
|
|
||||||
# display_data: Dict[str, Optional[TProfile]] = {f' {rows[0]}': None, f' {rows[1]}': None}
|
|
||||||
#
|
|
||||||
# for row, profile in zip(rows[2:], data):
|
|
||||||
# row = row.replace('|', '\\|')
|
|
||||||
# display_data[row] = profile
|
|
||||||
#
|
|
||||||
# return display_data
|
|
||||||
#
|
|
||||||
# def selected_action_display(self, profile: TProfile) -> str:
|
|
||||||
# return profile.name
|
|
||||||
#
|
|
||||||
# def handle_action(
|
|
||||||
# self,
|
|
||||||
# action: str,
|
|
||||||
# entry: Optional['CustomTypeProfile'],
|
|
||||||
# data: List['CustomTypeProfile']
|
|
||||||
# ) -> List['CustomTypeProfile']:
|
|
||||||
# if action == self._actions[0]: # add
|
|
||||||
# new_profile = self._add_profile()
|
|
||||||
# if new_profile is not None:
|
|
||||||
# # in case a profile with the same name as an existing profile
|
|
||||||
# # was created we'll replace the existing one
|
|
||||||
# data = [d for d in data if d.name != new_profile.name]
|
|
||||||
# data += [new_profile]
|
|
||||||
# elif entry is not None:
|
|
||||||
# if action == self._actions[1]: # edit
|
|
||||||
# new_profile = self._add_profile(entry)
|
|
||||||
# if new_profile is not None:
|
|
||||||
# # we'll remove the original profile and add the modified version
|
|
||||||
# data = [d for d in data if d.name != entry.name and d.name != new_profile.name]
|
|
||||||
# data += [new_profile]
|
|
||||||
# elif action == self._actions[2]: # delete
|
|
||||||
# data = [d for d in data if d != entry]
|
|
||||||
#
|
|
||||||
# return data
|
|
||||||
#
|
|
||||||
# def _is_new_profile_name(self, name: str) -> bool:
|
|
||||||
# existing_profile = profile_handler.get_profile_by_name(name)
|
|
||||||
# if existing_profile is not None and existing_profile.profile_type != ProfileType.CustomType:
|
|
||||||
# return False
|
|
||||||
# return True
|
|
||||||
#
|
|
||||||
# def _add_profile(self, editing: Optional['CustomTypeProfile'] = None) -> Optional['CustomTypeProfile']:
|
|
||||||
# name_prompt = '\n\n' + str(_('Profile name: '))
|
|
||||||
#
|
|
||||||
# while True:
|
|
||||||
# profile_name = menu.TextInput(name_prompt, editing.name if editing else '').run().strip()
|
|
||||||
#
|
|
||||||
# if not profile_name:
|
|
||||||
# return None
|
|
||||||
#
|
|
||||||
# if not self._is_new_profile_name(profile_name):
|
|
||||||
# error_prompt = str(_("The profile name you entered is already in use. Try again"))
|
|
||||||
# print(error_prompt)
|
|
||||||
# else:
|
|
||||||
# break
|
|
||||||
#
|
|
||||||
# packages_prompt = str(_('Packages to be install with this profile (space separated, leave blank to skip): '))
|
|
||||||
# edit_packages = ' '.join(editing.packages) if editing else ''
|
|
||||||
# packages = menu.TextInput(packages_prompt, edit_packages).run().strip()
|
|
||||||
#
|
|
||||||
# services_prompt = str(_('Services to be enabled with this profile (space separated, leave blank to skip): '))
|
|
||||||
# edit_services = ' '.join(editing.services) if editing else ''
|
|
||||||
# services = menu.TextInput(services_prompt, edit_services).run().strip()
|
|
||||||
#
|
|
||||||
# choice = menu.Menu(
|
|
||||||
# str(_('Should this profile be enabled for installation?')),
|
|
||||||
# menu.Menu.yes_no(),
|
|
||||||
# skip=False,
|
|
||||||
# default_option=menu.Menu.no(),
|
|
||||||
# clear_screen=False,
|
|
||||||
# show_search_hint=False
|
|
||||||
# ).run()
|
|
||||||
#
|
|
||||||
# enable_profile = True if choice.value == menu.Menu.yes() else False
|
|
||||||
#
|
|
||||||
# profile = CustomTypeProfile(
|
|
||||||
# profile_name,
|
|
||||||
# enabled=enable_profile,
|
|
||||||
# packages=packages.split(' '),
|
|
||||||
# services=services.split(' ')
|
|
||||||
# )
|
|
||||||
#
|
|
||||||
# return profile
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# # TODO
|
|
||||||
# # Still needs some ironing out
|
|
||||||
# class CustomProfile():
|
|
||||||
# def __init__(self):
|
|
||||||
# super().__init__(
|
|
||||||
# 'Custom',
|
|
||||||
# ProfileType.Custom,
|
|
||||||
# )
|
|
||||||
#
|
|
||||||
# def json(self) -> Dict[str, Any]:
|
|
||||||
# data: Dict[str, Any] = {'main': self.name, 'gfx_driver': self.gfx_driver, 'custom': []}
|
|
||||||
#
|
|
||||||
# for profile in self._current_selection:
|
|
||||||
# data['custom'].append({
|
|
||||||
# 'name': profile.name,
|
|
||||||
# 'packages': profile.packages,
|
|
||||||
# 'services': profile.services,
|
|
||||||
# 'enabled': profile.custom_enabled
|
|
||||||
# })
|
|
||||||
#
|
|
||||||
# return data
|
|
||||||
#
|
|
||||||
# def do_on_select(self) -> SelectResult:
|
|
||||||
# custom_profile_list = CustomProfileList('', profile_handler.get_custom_profiles())
|
|
||||||
# custom_profiles = custom_profile_list.run()
|
|
||||||
#
|
|
||||||
# # we'll first remove existing custom default_profiles with
|
|
||||||
# # the same name and then add the new ones this
|
|
||||||
# # will avoid errors of default_profiles with duplicate naming
|
|
||||||
# profile_handler.remove_custom_profiles(custom_profiles)
|
|
||||||
# profile_handler.add_custom_profiles(custom_profiles)
|
|
||||||
#
|
|
||||||
# self.set_current_selection(custom_profiles)
|
|
||||||
#
|
|
||||||
# if custom_profile_list.is_last_choice_cancel():
|
|
||||||
# return SelectResult.SameSelection
|
|
||||||
#
|
|
||||||
# enabled_profiles = [p for p in self._current_selection if p.custom_enabled]
|
|
||||||
# # in case we only created inactive default_profiles we wanna store them but
|
|
||||||
# # we want to reset the original setting
|
|
||||||
# if not enabled_profiles:
|
|
||||||
# return SelectResult.ResetCurrent
|
|
||||||
#
|
|
||||||
# return SelectResult.NewSelection
|
|
||||||
#
|
|
||||||
# def post_install(self, install_session: 'Installer'):
|
|
||||||
# for profile in self._current_selection:
|
|
||||||
# profile.post_install(install_session)
|
|
||||||
#
|
|
||||||
# def install(self, install_session: 'Installer'):
|
|
||||||
# driver_packages = self.gfx_driver_packages()
|
|
||||||
# install_session.add_additional_packages(driver_packages)
|
|
||||||
#
|
|
||||||
# for profile in self._current_selection:
|
|
||||||
# if profile.custom_enabled:
|
|
||||||
# log(f'Installing custom profile {profile.name}...')
|
|
||||||
#
|
|
||||||
# install_session.add_additional_packages(profile.packages)
|
|
||||||
# install_session.enable_service(profile.services)
|
|
||||||
#
|
|
||||||
# profile.install(install_session)
|
|
||||||
#
|
|
||||||
# def info(self) -> Optional[ProfileInfo]:
|
|
||||||
# enabled_profiles = [p for p in self._current_selection if p.custom_enabled]
|
|
||||||
# if enabled_profiles:
|
|
||||||
# details = ', '.join([p.name for p in enabled_profiles])
|
|
||||||
# gfx_driver = self.gfx_driver
|
|
||||||
# return ProfileInfo(self.name, details, gfx_driver)
|
|
||||||
#
|
|
||||||
# return None
|
|
||||||
#
|
|
||||||
# def reset(self):
|
|
||||||
# for profile in self._current_selection:
|
|
||||||
# profile.set_enabled(False)
|
|
||||||
#
|
|
||||||
# self.gfx_driver = None
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# class CustomTypeProfile(Profile):
|
|
||||||
# def __init__(
|
|
||||||
# self,
|
|
||||||
# name: str,
|
|
||||||
# enabled: bool = False,
|
|
||||||
# packages: List[str] = [],
|
|
||||||
# services: List[str] = []
|
|
||||||
# ):
|
|
||||||
# super().__init__(
|
|
||||||
# name,
|
|
||||||
# ProfileType.CustomType,
|
|
||||||
# packages=packages,
|
|
||||||
# services=services,
|
|
||||||
# support_gfx_driver=True
|
|
||||||
# )
|
|
||||||
#
|
|
||||||
# self.custom_enabled = enabled
|
|
||||||
#
|
|
||||||
# def json(self) -> Dict[str, Any]:
|
|
||||||
# return {
|
|
||||||
# 'name': self.name,
|
|
||||||
# 'packages': self.packages,
|
|
||||||
# 'services': self.services,
|
|
||||||
# 'enabled': self.custom_enabled
|
|
||||||
# }
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
from typing import TYPE_CHECKING, override
|
from typing import TYPE_CHECKING, Self, override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, Profile, ProfileType, SelectResult
|
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType, SelectResult
|
||||||
from archinstall.lib.output import info
|
from archinstall.lib.log import info
|
||||||
|
from archinstall.lib.menu.helpers import Selection
|
||||||
from archinstall.lib.profile.profiles_handler import profile_handler
|
from archinstall.lib.profile.profiles_handler import profile_handler
|
||||||
from archinstall.tui.curses_menu import SelectMenu
|
|
||||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||||
from archinstall.tui.result import ResultType
|
from archinstall.tui.result import ResultType
|
||||||
from archinstall.tui.types import FrameProperties, PreviewStyle
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from archinstall.lib.installer import Installer
|
from archinstall.lib.installer import Installer
|
||||||
|
from archinstall.lib.models.users import User
|
||||||
|
|
||||||
|
|
||||||
class DesktopProfile(Profile):
|
class DesktopProfile(Profile):
|
||||||
def __init__(self, current_selection: list[Profile] = []) -> None:
|
def __init__(self, current_selection: list[Self] = []) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
'Desktop',
|
'Desktop',
|
||||||
ProfileType.Desktop,
|
ProfileType.Desktop,
|
||||||
|
|
@ -30,9 +30,6 @@ class DesktopProfile(Profile):
|
||||||
'openssh',
|
'openssh',
|
||||||
'htop',
|
'htop',
|
||||||
'wget',
|
'wget',
|
||||||
'iwd',
|
|
||||||
'wireless_tools',
|
|
||||||
'wpa_supplicant',
|
|
||||||
'smartmontools',
|
'smartmontools',
|
||||||
'xdg-utils',
|
'xdg-utils',
|
||||||
]
|
]
|
||||||
|
|
@ -51,17 +48,17 @@ class DesktopProfile(Profile):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _do_on_select_profiles(self) -> None:
|
async def _do_on_select_profiles(self) -> None:
|
||||||
for profile in self.current_selection:
|
for profile in self.current_selection:
|
||||||
profile.do_on_select()
|
await profile.do_on_select()
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def do_on_select(self) -> SelectResult:
|
async def do_on_select(self) -> SelectResult:
|
||||||
items = [
|
items = [
|
||||||
MenuItem(
|
MenuItem(
|
||||||
p.name,
|
p.name,
|
||||||
value=p,
|
value=p,
|
||||||
preview_action=lambda x: x.value.preview_text(),
|
preview_action=lambda x: x.value.preview_text() if x.value else None,
|
||||||
)
|
)
|
||||||
for p in profile_handler.get_desktop_profiles()
|
for p in profile_handler.get_desktop_profiles()
|
||||||
]
|
]
|
||||||
|
|
@ -69,20 +66,18 @@ class DesktopProfile(Profile):
|
||||||
group = MenuItemGroup(items, sort_items=True, sort_case_sensitive=False)
|
group = MenuItemGroup(items, sort_items=True, sort_case_sensitive=False)
|
||||||
group.set_selected_by_value(self.current_selection)
|
group.set_selected_by_value(self.current_selection)
|
||||||
|
|
||||||
result = SelectMenu[Profile](
|
result = await Selection[Self](
|
||||||
group,
|
group,
|
||||||
multi=True,
|
multi=True,
|
||||||
allow_reset=True,
|
allow_reset=True,
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
preview_style=PreviewStyle.RIGHT,
|
preview_location='right',
|
||||||
preview_size='auto',
|
).show()
|
||||||
preview_frame=FrameProperties.max('Info'),
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
match result.type_:
|
||||||
case ResultType.Selection:
|
case ResultType.Selection:
|
||||||
self.current_selection = result.get_values()
|
self.current_selection = result.get_values()
|
||||||
self._do_on_select_profiles()
|
await self._do_on_select_profiles()
|
||||||
return SelectResult.NewSelection
|
return SelectResult.NewSelection
|
||||||
case ResultType.Skip:
|
case ResultType.Skip:
|
||||||
return SelectResult.SameSelection
|
return SelectResult.SameSelection
|
||||||
|
|
@ -90,19 +85,30 @@ class DesktopProfile(Profile):
|
||||||
return SelectResult.ResetCurrent
|
return SelectResult.ResetCurrent
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def post_install(self, install_session: 'Installer') -> None:
|
def post_install(self, install_session: Installer) -> None:
|
||||||
for profile in self.current_selection:
|
for profile in self.current_selection:
|
||||||
profile.post_install(install_session)
|
profile.post_install(install_session)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def install(self, install_session: 'Installer') -> None:
|
def provision(self, install_session: Installer, users: list[User]) -> None:
|
||||||
|
for profile in self.current_selection:
|
||||||
|
profile.provision(install_session, users)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def install(self, install_session: Installer) -> None:
|
||||||
# Install common packages for all desktop environments
|
# Install common packages for all desktop environments
|
||||||
install_session.add_additional_packages(self.packages)
|
install_session.add_additional_packages(self.packages)
|
||||||
|
|
||||||
|
xorg_installed = False
|
||||||
|
|
||||||
for profile in self.current_selection:
|
for profile in self.current_selection:
|
||||||
info(f'Installing profile {profile.name}...')
|
info(f'Installing profile {profile.name}...')
|
||||||
|
|
||||||
install_session.add_additional_packages(profile.packages)
|
install_session.add_additional_packages(profile.packages)
|
||||||
install_session.enable_service(profile.services)
|
install_session.enable_service(profile.services)
|
||||||
|
|
||||||
|
if not xorg_installed and profile.display_server == DisplayServerType.Xorg:
|
||||||
|
install_session.add_additional_packages(['xorg-server', 'xorg-xinit'])
|
||||||
|
xorg_installed = True
|
||||||
|
|
||||||
profile.install(install_session)
|
profile.install(install_session)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
class SeatAccess(Enum):
|
|
||||||
seatd = 'seatd'
|
|
||||||
polkit = 'polkit'
|
|
||||||
|
|
@ -1,23 +1,26 @@
|
||||||
from typing import TYPE_CHECKING, override
|
from typing import TYPE_CHECKING, override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import ProfileType
|
from archinstall.default_profiles.profile import DisplayServerType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from archinstall.lib.installer import Installer
|
from archinstall.lib.installer import Installer
|
||||||
|
|
||||||
|
|
||||||
class AwesomeProfile(XorgProfile):
|
class AwesomeProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('Awesome', ProfileType.WindowMgr)
|
super().__init__(
|
||||||
|
'Awesome',
|
||||||
|
ProfileType.WindowMgr,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Xorg,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def packages(self) -> list[str]:
|
def packages(self) -> list[str]:
|
||||||
return super().packages + [
|
return [
|
||||||
'awesome',
|
'awesome',
|
||||||
'alacritty',
|
'alacritty',
|
||||||
'xorg-xinit',
|
|
||||||
'xorg-xrandr',
|
'xorg-xrandr',
|
||||||
'xterm',
|
'xterm',
|
||||||
'feh',
|
'feh',
|
||||||
|
|
@ -29,7 +32,7 @@ class AwesomeProfile(XorgProfile):
|
||||||
]
|
]
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def install(self, install_session: 'Installer') -> None:
|
def install(self, install_session: Installer) -> None:
|
||||||
super().install(install_session)
|
super().install(install_session)
|
||||||
|
|
||||||
# TODO: Copy a full configuration to ~/.config/awesome/rc.lua instead.
|
# TODO: Copy a full configuration to ~/.config/awesome/rc.lua instead.
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
from archinstall.lib.installer import Installer
|
||||||
|
from archinstall.lib.models.users import User
|
||||||
|
|
||||||
|
|
||||||
class BspwmProfile(XorgProfile):
|
class BspwmProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('Bspwm', ProfileType.WindowMgr)
|
super().__init__(
|
||||||
|
'Bspwm',
|
||||||
|
ProfileType.WindowMgr,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Xorg,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def packages(self) -> list[str]:
|
def packages(self) -> list[str]:
|
||||||
# return super().packages + [
|
|
||||||
return [
|
return [
|
||||||
'bspwm',
|
'bspwm',
|
||||||
'sxhkd',
|
'sxhkd',
|
||||||
|
|
@ -24,3 +29,11 @@ class BspwmProfile(XorgProfile):
|
||||||
@override
|
@override
|
||||||
def default_greeter_type(self) -> GreeterType:
|
def default_greeter_type(self) -> GreeterType:
|
||||||
return GreeterType.Lightdm
|
return GreeterType.Lightdm
|
||||||
|
|
||||||
|
@override
|
||||||
|
def provision(self, install_session: Installer, users: list[User]) -> None:
|
||||||
|
for user in users:
|
||||||
|
install_session.arch_chroot('mkdir -p ~/.config/bspwm ~/.config/sxhkd', run_as=user.username)
|
||||||
|
install_session.arch_chroot('cp /usr/share/doc/bspwm/examples/bspwmrc ~/.config/bspwm/', run_as=user.username)
|
||||||
|
install_session.arch_chroot('cp /usr/share/doc/bspwm/examples/sxhkdrc ~/.config/sxhkd/', run_as=user.username)
|
||||||
|
install_session.arch_chroot('chmod +x ~/.config/bspwm/bspwmrc', run_as=user.username)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
|
|
||||||
|
|
||||||
class BudgieProfile(XorgProfile):
|
class BudgieProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('Budgie', ProfileType.DesktopEnv)
|
super().__init__(
|
||||||
|
'Budgie',
|
||||||
|
ProfileType.DesktopEnv,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Wayland,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
|
|
@ -16,6 +20,7 @@ class BudgieProfile(XorgProfile):
|
||||||
'budgie',
|
'budgie',
|
||||||
'mate-terminal',
|
'mate-terminal',
|
||||||
'nemo',
|
'nemo',
|
||||||
|
'nemo-fileroller',
|
||||||
'papirus-icon-theme',
|
'papirus-icon-theme',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
|
|
||||||
|
|
||||||
class CinnamonProfile(XorgProfile):
|
class CinnamonProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('Cinnamon', ProfileType.DesktopEnv)
|
super().__init__(
|
||||||
|
'Cinnamon',
|
||||||
|
ProfileType.DesktopEnv,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Xorg,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
|
|
||||||
|
|
||||||
class CosmicProfile(XorgProfile):
|
class CosmicProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('cosmic-epoch', ProfileType.DesktopEnv, advanced=True)
|
super().__init__(
|
||||||
|
'Cosmic',
|
||||||
|
ProfileType.DesktopEnv,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Wayland,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
from typing import override
|
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
|
|
||||||
|
|
||||||
class CutefishProfile(XorgProfile):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__('Cutefish', ProfileType.DesktopEnv)
|
|
||||||
|
|
||||||
@property
|
|
||||||
@override
|
|
||||||
def packages(self) -> list[str]:
|
|
||||||
return [
|
|
||||||
'cutefish',
|
|
||||||
'noto-fonts',
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
|
||||||
@override
|
|
||||||
def default_greeter_type(self) -> GreeterType:
|
|
||||||
return GreeterType.Sddm
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
|
|
||||||
|
|
||||||
class DeepinProfile(XorgProfile):
|
class DeepinProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('Deepin', ProfileType.DesktopEnv)
|
super().__init__(
|
||||||
|
'Deepin',
|
||||||
|
ProfileType.DesktopEnv,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Xorg,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
|
|
||||||
|
|
||||||
class EnlighenmentProfile(XorgProfile):
|
class EnlightenmentProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('Enlightenment', ProfileType.WindowMgr)
|
super().__init__(
|
||||||
|
'Enlightenment',
|
||||||
|
ProfileType.WindowMgr,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Xorg,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
|
|
||||||
|
|
||||||
class GnomeProfile(XorgProfile):
|
class GnomeProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('GNOME', ProfileType.DesktopEnv)
|
super().__init__(
|
||||||
|
'GNOME',
|
||||||
|
ProfileType.DesktopEnv,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Wayland,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,19 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.desktops import SeatAccess
|
from archinstall.default_profiles.desktops.utils import select_seat_access
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
from archinstall.lib.translationhandler import tr
|
|
||||||
from archinstall.tui.curses_menu import SelectMenu
|
|
||||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
|
||||||
from archinstall.tui.result import ResultType
|
|
||||||
from archinstall.tui.types import Alignment, FrameProperties
|
|
||||||
|
|
||||||
|
|
||||||
class HyprlandProfile(XorgProfile):
|
class HyprlandProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('Hyprland', ProfileType.DesktopEnv)
|
super().__init__(
|
||||||
|
'Hyprland',
|
||||||
|
ProfileType.DesktopEnv,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Wayland,
|
||||||
|
)
|
||||||
|
|
||||||
self.custom_settings = {'seat_access': None}
|
self.custom_settings = {CustomSetting.SeatAccess: None}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
|
|
@ -42,33 +41,12 @@ class HyprlandProfile(XorgProfile):
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def services(self) -> list[str]:
|
def services(self) -> list[str]:
|
||||||
if pref := self.custom_settings.get('seat_access', None):
|
if pref := self.custom_settings.get(CustomSetting.SeatAccess, None):
|
||||||
return [pref]
|
return [pref]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _ask_seat_access(self) -> None:
|
|
||||||
# need to activate seat service and add to seat group
|
|
||||||
header = tr('Hyprland needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)')
|
|
||||||
header += '\n' + tr('Choose an option to give Hyprland access to your hardware') + '\n'
|
|
||||||
|
|
||||||
items = [MenuItem(s.value, value=s) for s in SeatAccess]
|
|
||||||
group = MenuItemGroup(items, sort_items=True)
|
|
||||||
|
|
||||||
default = self.custom_settings.get('seat_access', None)
|
|
||||||
group.set_default_by_value(default)
|
|
||||||
|
|
||||||
result = SelectMenu[SeatAccess](
|
|
||||||
group,
|
|
||||||
header=header,
|
|
||||||
allow_skip=False,
|
|
||||||
frame=FrameProperties.min(tr('Seat access')),
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
if result.type_ == ResultType.Selection:
|
|
||||||
self.custom_settings['seat_access'] = result.get_value().value
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def do_on_select(self) -> None:
|
async def do_on_select(self) -> None:
|
||||||
self._ask_seat_access()
|
default = self.custom_settings.get(CustomSetting.SeatAccess, None)
|
||||||
return None
|
seat_access = await select_seat_access(self.name, default)
|
||||||
|
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
|
|
||||||
|
|
||||||
class I3wmProfile(XorgProfile):
|
class I3wmProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('i3-wm', ProfileType.WindowMgr)
|
super().__init__(
|
||||||
|
'i3-wm',
|
||||||
|
ProfileType.WindowMgr,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Xorg,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,25 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.desktops import SeatAccess
|
from archinstall.default_profiles.desktops.utils import select_seat_access
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
from archinstall.lib.translationhandler import tr
|
|
||||||
from archinstall.tui.curses_menu import SelectMenu
|
|
||||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
|
||||||
from archinstall.tui.result import ResultType
|
|
||||||
from archinstall.tui.types import Alignment, FrameProperties
|
|
||||||
|
|
||||||
|
|
||||||
class LabwcProfile(XorgProfile):
|
class LabwcProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
'Labwc',
|
'Labwc',
|
||||||
ProfileType.WindowMgr,
|
ProfileType.WindowMgr,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Wayland,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.custom_settings = {'seat_access': None}
|
self.custom_settings = {CustomSetting.SeatAccess: None}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def packages(self) -> list[str]:
|
def packages(self) -> list[str]:
|
||||||
additional = []
|
additional = []
|
||||||
if seat := self.custom_settings.get('seat_access', None):
|
if seat := self.custom_settings.get(CustomSetting.SeatAccess, None):
|
||||||
additional = [seat]
|
additional = [seat]
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
@ -39,33 +35,12 @@ class LabwcProfile(XorgProfile):
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def services(self) -> list[str]:
|
def services(self) -> list[str]:
|
||||||
if pref := self.custom_settings.get('seat_access', None):
|
if pref := self.custom_settings.get(CustomSetting.SeatAccess, None):
|
||||||
return [pref]
|
return [pref]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _ask_seat_access(self) -> None:
|
|
||||||
# need to activate seat service and add to seat group
|
|
||||||
header = tr('labwc needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)')
|
|
||||||
header += '\n' + tr('Choose an option to give labwc access to your hardware') + '\n'
|
|
||||||
|
|
||||||
items = [MenuItem(s.value, value=s) for s in SeatAccess]
|
|
||||||
group = MenuItemGroup(items, sort_items=True)
|
|
||||||
|
|
||||||
default = self.custom_settings.get('seat_access', None)
|
|
||||||
group.set_default_by_value(default)
|
|
||||||
|
|
||||||
result = SelectMenu[SeatAccess](
|
|
||||||
group,
|
|
||||||
header=header,
|
|
||||||
allow_skip=False,
|
|
||||||
frame=FrameProperties.min(tr('Seat access')),
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
if result.type_ == ResultType.Selection:
|
|
||||||
self.custom_settings['seat_access'] = result.get_value().value
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def do_on_select(self) -> None:
|
async def do_on_select(self) -> None:
|
||||||
self._ask_seat_access()
|
default = self.custom_settings.get(CustomSetting.SeatAccess, None)
|
||||||
return None
|
seat_access = await select_seat_access(self.name, default)
|
||||||
|
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
|
|
||||||
|
|
||||||
class LxqtProfile(XorgProfile):
|
class LxqtProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('Lxqt', ProfileType.DesktopEnv)
|
super().__init__(
|
||||||
|
'Lxqt',
|
||||||
|
ProfileType.DesktopEnv,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Xorg,
|
||||||
|
)
|
||||||
|
|
||||||
# NOTE: SDDM is the only officially supported greeter for LXQt, so unlike other DEs, lightdm is not used here.
|
# NOTE: SDDM is the only officially supported greeter for LXQt, so unlike other DEs, lightdm is not used here.
|
||||||
# LXQt works with lightdm, but since this is not supported, we will not default to this.
|
# LXQt works with lightdm, but since this is not supported, we will not default to this.
|
||||||
|
|
@ -20,7 +24,7 @@ class LxqtProfile(XorgProfile):
|
||||||
'oxygen-icons',
|
'oxygen-icons',
|
||||||
'xdg-utils',
|
'xdg-utils',
|
||||||
'ttf-freefont',
|
'ttf-freefont',
|
||||||
'leafpad',
|
'l3afpad',
|
||||||
'slock',
|
'slock',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
|
|
||||||
|
|
||||||
class MateProfile(XorgProfile):
|
class MateProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('Mate', ProfileType.DesktopEnv)
|
super().__init__(
|
||||||
|
'Mate',
|
||||||
|
ProfileType.DesktopEnv,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Xorg,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,25 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.desktops import SeatAccess
|
from archinstall.default_profiles.desktops.utils import select_seat_access
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
from archinstall.lib.translationhandler import tr
|
|
||||||
from archinstall.tui.curses_menu import SelectMenu
|
|
||||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
|
||||||
from archinstall.tui.result import ResultType
|
|
||||||
from archinstall.tui.types import Alignment, FrameProperties
|
|
||||||
|
|
||||||
|
|
||||||
class NiriProfile(XorgProfile):
|
class NiriProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
'Niri',
|
'niri',
|
||||||
ProfileType.WindowMgr,
|
ProfileType.WindowMgr,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Wayland,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.custom_settings = {'seat_access': None}
|
self.custom_settings = {CustomSetting.SeatAccess: None}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def packages(self) -> list[str]:
|
def packages(self) -> list[str]:
|
||||||
additional = []
|
additional = []
|
||||||
if seat := self.custom_settings.get('seat_access', None):
|
if seat := self.custom_settings.get(CustomSetting.SeatAccess, None):
|
||||||
additional = [seat]
|
additional = [seat]
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
@ -47,33 +43,12 @@ class NiriProfile(XorgProfile):
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def services(self) -> list[str]:
|
def services(self) -> list[str]:
|
||||||
if pref := self.custom_settings.get('seat_access', None):
|
if pref := self.custom_settings.get(CustomSetting.SeatAccess, None):
|
||||||
return [pref]
|
return [pref]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _ask_seat_access(self) -> None:
|
|
||||||
# need to activate seat service and add to seat group
|
|
||||||
header = tr('niri needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)')
|
|
||||||
header += '\n' + tr('Choose an option to give niri access to your hardware') + '\n'
|
|
||||||
|
|
||||||
items = [MenuItem(s.value, value=s) for s in SeatAccess]
|
|
||||||
group = MenuItemGroup(items, sort_items=True)
|
|
||||||
|
|
||||||
default = self.custom_settings.get('seat_access', None)
|
|
||||||
group.set_default_by_value(default)
|
|
||||||
|
|
||||||
result = SelectMenu[SeatAccess](
|
|
||||||
group,
|
|
||||||
header=header,
|
|
||||||
allow_skip=False,
|
|
||||||
frame=FrameProperties.min(tr('Seat access')),
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
if result.type_ == ResultType.Selection:
|
|
||||||
self.custom_settings['seat_access'] = result.get_value().value
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def do_on_select(self) -> None:
|
async def do_on_select(self) -> None:
|
||||||
self._ask_seat_access()
|
default = self.custom_settings.get(CustomSetting.SeatAccess, None)
|
||||||
return None
|
seat_access = await select_seat_access(self.name, default)
|
||||||
|
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
// Place cursor configuration here.
|
||||||
|
// Example:
|
||||||
|
// cursor {
|
||||||
|
// xcursor-theme "Adwaita"
|
||||||
|
// xcursor-size 24
|
||||||
|
// }
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
// Place per-output configuration here.
|
||||||
|
// Example:
|
||||||
|
// output "DP-1" {
|
||||||
|
// mode "2560x1440@165"
|
||||||
|
// position x=0 y=0
|
||||||
|
// scale 1
|
||||||
|
// }
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -1,26 +1,120 @@
|
||||||
|
from enum import StrEnum
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
from archinstall.lib.menu.helpers import Selection
|
||||||
|
from archinstall.lib.packages.packages import available_package, package_group_info
|
||||||
|
from archinstall.lib.translationhandler import tr
|
||||||
|
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||||
|
from archinstall.tui.result import ResultType
|
||||||
|
|
||||||
|
|
||||||
class PlasmaProfile(XorgProfile):
|
class PlasmaFlavor(StrEnum):
|
||||||
|
Meta = 'plasma-meta'
|
||||||
|
Plasma = 'plasma'
|
||||||
|
Desktop = 'plasma-desktop'
|
||||||
|
|
||||||
|
def show(self) -> str:
|
||||||
|
match self:
|
||||||
|
case PlasmaFlavor.Meta:
|
||||||
|
return f'{self.value} ({tr("Recommended")})'
|
||||||
|
case PlasmaFlavor.Plasma | PlasmaFlavor.Desktop:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def package_details(self) -> str:
|
||||||
|
ty = ''
|
||||||
|
details = ''
|
||||||
|
desc = ''
|
||||||
|
|
||||||
|
match self:
|
||||||
|
case PlasmaFlavor.Meta:
|
||||||
|
ty = tr('Package')
|
||||||
|
desc = tr('Curated selection of KDE Plasma packages')
|
||||||
|
info = available_package(self.value)
|
||||||
|
|
||||||
|
if info is not None:
|
||||||
|
details = tr('Dependencies') + '\n'
|
||||||
|
details += '\n'.join(f'- {entry}' for entry in info.get_depends_on)
|
||||||
|
case PlasmaFlavor.Plasma:
|
||||||
|
ty = tr('Package group')
|
||||||
|
desc = tr('Extensive KDE Plasma installation')
|
||||||
|
group = package_group_info(self.value)
|
||||||
|
|
||||||
|
if group is not None:
|
||||||
|
details = tr('Packages in group') + '\n'
|
||||||
|
details += '\n'.join(f'- {entry}' for entry in group.packages)
|
||||||
|
case PlasmaFlavor.Desktop:
|
||||||
|
ty = tr('Package group')
|
||||||
|
desc = tr('Minimal KDE Plasma installation')
|
||||||
|
info = available_package(self.value)
|
||||||
|
|
||||||
|
if info is not None:
|
||||||
|
details = tr('Dependencies') + '\n'
|
||||||
|
details += '\n'.join(f'- {entry}' for entry in info.get_depends_on)
|
||||||
|
|
||||||
|
return f'{tr("Type")}: {ty}\n{tr("Description")}: {desc}\n\n{details}'
|
||||||
|
|
||||||
|
def packages(self) -> list[str]:
|
||||||
|
match self:
|
||||||
|
case PlasmaFlavor.Meta:
|
||||||
|
return ['plasma-meta']
|
||||||
|
case PlasmaFlavor.Plasma:
|
||||||
|
return ['plasma']
|
||||||
|
case PlasmaFlavor.Desktop:
|
||||||
|
return ['plasma-desktop']
|
||||||
|
|
||||||
|
|
||||||
|
class PlasmaProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('KDE Plasma', ProfileType.DesktopEnv)
|
super().__init__(
|
||||||
|
'KDE Plasma',
|
||||||
|
ProfileType.DesktopEnv,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Wayland,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def packages(self) -> list[str]:
|
def packages(self) -> list[str]:
|
||||||
return [
|
flavor_str = self.custom_settings.get(CustomSetting.PlasmaFlavor)
|
||||||
'plasma-meta',
|
|
||||||
'konsole',
|
if flavor_str is not None:
|
||||||
'kate',
|
flavor = PlasmaFlavor(flavor_str)
|
||||||
'dolphin',
|
return flavor.packages()
|
||||||
'ark',
|
else:
|
||||||
'plasma-workspace',
|
return PlasmaFlavor.Meta.packages() # use plasma-meta as the recommended default
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def default_greeter_type(self) -> GreeterType:
|
def default_greeter_type(self) -> GreeterType:
|
||||||
return GreeterType.Sddm
|
return GreeterType.PlasmaLoginManager
|
||||||
|
|
||||||
|
async def _select_flavor(self) -> None:
|
||||||
|
header = tr('Select a flavor of KDE Plasma to install') + '\n'
|
||||||
|
|
||||||
|
items = [
|
||||||
|
MenuItem(
|
||||||
|
s.show(),
|
||||||
|
value=s,
|
||||||
|
preview_action=lambda x: x.value.package_details() if x.value else None,
|
||||||
|
)
|
||||||
|
for s in PlasmaFlavor
|
||||||
|
]
|
||||||
|
group = MenuItemGroup(items, sort_items=False)
|
||||||
|
|
||||||
|
default = self.custom_settings.get(CustomSetting.PlasmaFlavor, None)
|
||||||
|
group.set_default_by_value(default)
|
||||||
|
|
||||||
|
result = await Selection[PlasmaFlavor](
|
||||||
|
group,
|
||||||
|
header=header,
|
||||||
|
allow_skip=False,
|
||||||
|
preview_location='right',
|
||||||
|
).show()
|
||||||
|
|
||||||
|
if result.type_ == ResultType.Selection:
|
||||||
|
self.custom_settings[CustomSetting.PlasmaFlavor] = result.get_value().value
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def do_on_select(self) -> None:
|
||||||
|
await self._select_flavor()
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
|
|
||||||
|
|
||||||
class QtileProfile(XorgProfile):
|
class QtileProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('Qtile', ProfileType.WindowMgr)
|
super().__init__(
|
||||||
|
'Qtile',
|
||||||
|
ProfileType.WindowMgr,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Xorg,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
|
|
||||||
|
|
||||||
class RiverProfile(XorgProfile):
|
class RiverProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('River', ProfileType.WindowMgr)
|
super().__init__(
|
||||||
|
'River',
|
||||||
|
ProfileType.WindowMgr,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Wayland,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,25 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.desktops import SeatAccess
|
from archinstall.default_profiles.desktops.utils import select_seat_access
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import CustomSetting, DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
from archinstall.lib.translationhandler import tr
|
|
||||||
from archinstall.tui.curses_menu import SelectMenu
|
|
||||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
|
||||||
from archinstall.tui.result import ResultType
|
|
||||||
from archinstall.tui.types import Alignment, FrameProperties
|
|
||||||
|
|
||||||
|
|
||||||
class SwayProfile(XorgProfile):
|
class SwayProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
'Sway',
|
'Sway',
|
||||||
ProfileType.WindowMgr,
|
ProfileType.WindowMgr,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Wayland,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.custom_settings = {'seat_access': None}
|
self.custom_settings = {CustomSetting.SeatAccess: None}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def packages(self) -> list[str]:
|
def packages(self) -> list[str]:
|
||||||
additional = []
|
additional = []
|
||||||
if seat := self.custom_settings.get('seat_access', None):
|
if seat := self.custom_settings.get(CustomSetting.SeatAccess, None):
|
||||||
additional = [seat]
|
additional = [seat]
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
@ -49,33 +45,12 @@ class SwayProfile(XorgProfile):
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def services(self) -> list[str]:
|
def services(self) -> list[str]:
|
||||||
if pref := self.custom_settings.get('seat_access', None):
|
if pref := self.custom_settings.get(CustomSetting.SeatAccess, None):
|
||||||
return [pref]
|
return [pref]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _ask_seat_access(self) -> None:
|
|
||||||
# need to activate seat service and add to seat group
|
|
||||||
header = tr('Sway needs access to your seat (collection of hardware devices i.e. keyboard, mouse, etc)')
|
|
||||||
header += '\n' + tr('Choose an option to give Sway access to your hardware') + '\n'
|
|
||||||
|
|
||||||
items = [MenuItem(s.value, value=s) for s in SeatAccess]
|
|
||||||
group = MenuItemGroup(items, sort_items=True)
|
|
||||||
|
|
||||||
default = self.custom_settings.get('seat_access', None)
|
|
||||||
group.set_default_by_value(default)
|
|
||||||
|
|
||||||
result = SelectMenu[SeatAccess](
|
|
||||||
group,
|
|
||||||
header=header,
|
|
||||||
allow_skip=False,
|
|
||||||
frame=FrameProperties.min(tr('Seat access')),
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
if result.type_ == ResultType.Selection:
|
|
||||||
self.custom_settings['seat_access'] = result.get_value().value
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def do_on_select(self) -> None:
|
async def do_on_select(self) -> None:
|
||||||
self._ask_seat_access()
|
default = self.custom_settings.get(CustomSetting.SeatAccess, None)
|
||||||
return None
|
seat_access = await select_seat_access(self.name, default)
|
||||||
|
self.custom_settings[CustomSetting.SeatAccess] = seat_access.value
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from archinstall.lib.menu.helpers import Selection
|
||||||
|
from archinstall.lib.translationhandler import tr
|
||||||
|
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||||
|
from archinstall.tui.result import ResultType
|
||||||
|
|
||||||
|
|
||||||
|
class SeatAccess(Enum):
|
||||||
|
seatd = 'seatd'
|
||||||
|
polkit = 'polkit'
|
||||||
|
|
||||||
|
|
||||||
|
async def select_seat_access(profile_name: str, default: str | None) -> SeatAccess:
|
||||||
|
header = tr('{} needs access to your seat').format(profile_name)
|
||||||
|
header += f' ({tr("collection of hardware devices i.e. keyboard, mouse")})' + '\n'
|
||||||
|
header += tr('Choose an option how to give {} access to your hardware').format(profile_name)
|
||||||
|
|
||||||
|
items = [MenuItem(s.value, value=s) for s in SeatAccess]
|
||||||
|
group = MenuItemGroup(items, sort_items=True)
|
||||||
|
|
||||||
|
group.set_default_by_value(default)
|
||||||
|
|
||||||
|
result = await Selection[SeatAccess](
|
||||||
|
group,
|
||||||
|
header=header,
|
||||||
|
allow_skip=False,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
if result.type_ == ResultType.Selection:
|
||||||
|
return result.get_value()
|
||||||
|
else:
|
||||||
|
raise ValueError('Unexpected result type from seat access selection')
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
|
|
||||||
|
|
||||||
class Xfce4Profile(XorgProfile):
|
class Xfce4Profile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('Xfce4', ProfileType.DesktopEnv)
|
super().__init__(
|
||||||
|
'Xfce4',
|
||||||
|
ProfileType.DesktopEnv,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Xorg,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import GreeterType, ProfileType
|
from archinstall.default_profiles.profile import DisplayServerType, GreeterType, Profile, ProfileType
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
|
|
||||||
|
|
||||||
class XmonadProfile(XorgProfile):
|
class XmonadProfile(Profile):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__('Xmonad', ProfileType.WindowMgr)
|
super().__init__(
|
||||||
|
'Xmonad',
|
||||||
|
ProfileType.WindowMgr,
|
||||||
|
support_gfx_driver=True,
|
||||||
|
display_server=DisplayServerType.Xorg,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
from __future__ import annotations
|
from enum import Enum, StrEnum, auto
|
||||||
|
from typing import TYPE_CHECKING, Self
|
||||||
import sys
|
|
||||||
from enum import Enum, auto
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from archinstall.lib.translationhandler import tr
|
from archinstall.lib.translationhandler import tr
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..lib.installer import Installer
|
from archinstall.lib.installer import Installer
|
||||||
|
from archinstall.lib.models.users import User
|
||||||
|
|
||||||
|
|
||||||
|
class DisplayServerType(Enum):
|
||||||
|
Xorg = 'Xorg'
|
||||||
|
Wayland = 'Wayland'
|
||||||
|
|
||||||
|
|
||||||
class ProfileType(Enum):
|
class ProfileType(Enum):
|
||||||
|
|
@ -23,7 +26,6 @@ class ProfileType(Enum):
|
||||||
DesktopEnv = 'Desktop Environment'
|
DesktopEnv = 'Desktop Environment'
|
||||||
CustomType = 'CustomType'
|
CustomType = 'CustomType'
|
||||||
# special things
|
# special things
|
||||||
Tailored = 'Tailored'
|
|
||||||
Application = 'Application'
|
Application = 'Application'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -33,10 +35,9 @@ class GreeterType(Enum):
|
||||||
Sddm = 'sddm'
|
Sddm = 'sddm'
|
||||||
Gdm = 'gdm'
|
Gdm = 'gdm'
|
||||||
Ly = 'ly'
|
Ly = 'ly'
|
||||||
|
CosmicSession = 'cosmic-greeter'
|
||||||
# .. todo:: Remove when we un-hide cosmic behind --advanced
|
PlasmaLoginManager = 'plasma-login-manager'
|
||||||
if '--advanced' in sys.argv:
|
GreetdDms = 'dms-greeter'
|
||||||
CosmicSession = 'cosmic-greeter'
|
|
||||||
|
|
||||||
|
|
||||||
class SelectResult(Enum):
|
class SelectResult(Enum):
|
||||||
|
|
@ -45,25 +46,30 @@ class SelectResult(Enum):
|
||||||
ResetCurrent = auto()
|
ResetCurrent = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class CustomSetting(StrEnum):
|
||||||
|
SeatAccess = 'seat_access'
|
||||||
|
PlasmaFlavor = 'plasma_flavor'
|
||||||
|
|
||||||
|
|
||||||
class Profile:
|
class Profile:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
profile_type: ProfileType,
|
profile_type: ProfileType,
|
||||||
current_selection: list[Profile] = [],
|
current_selection: list[Self] = [],
|
||||||
packages: list[str] = [],
|
packages: list[str] = [],
|
||||||
services: list[str] = [],
|
services: list[str] = [],
|
||||||
support_gfx_driver: bool = False,
|
support_gfx_driver: bool = False,
|
||||||
support_greeter: bool = False,
|
support_greeter: bool = False,
|
||||||
advanced: bool = False,
|
display_server: DisplayServerType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.profile_type = profile_type
|
self.profile_type = profile_type
|
||||||
self.custom_settings: dict[str, str | None] = {}
|
self.custom_settings: dict[CustomSetting, str | None] = {}
|
||||||
self.advanced = advanced
|
|
||||||
|
|
||||||
self._support_gfx_driver = support_gfx_driver
|
self._support_gfx_driver = support_gfx_driver
|
||||||
self._support_greeter = support_greeter
|
self._support_greeter = support_greeter
|
||||||
|
self._display_server = display_server
|
||||||
|
|
||||||
# self.gfx_driver: str | None = None
|
# self.gfx_driver: str | None = None
|
||||||
|
|
||||||
|
|
@ -97,40 +103,38 @@ class Profile:
|
||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _advanced_check(self) -> bool:
|
def install(self, install_session: Installer) -> None:
|
||||||
"""
|
|
||||||
Used to control if the Profile() should be visible or not in different contexts.
|
|
||||||
Returns True if --advanced is given on a Profile(advanced=True) instance.
|
|
||||||
"""
|
|
||||||
from archinstall.lib.args import arch_config_handler
|
|
||||||
|
|
||||||
return self.advanced is False or arch_config_handler.args.advanced is True
|
|
||||||
|
|
||||||
def install(self, install_session: 'Installer') -> None:
|
|
||||||
"""
|
"""
|
||||||
Performs installation steps when this profile was selected
|
Performs installation steps when this profile was selected
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def post_install(self, install_session: 'Installer') -> None:
|
def post_install(self, install_session: Installer) -> None:
|
||||||
"""
|
"""
|
||||||
Hook that will be called when the installation process is
|
Hook that will be called when the installation process is
|
||||||
finished and custom installation steps for specific default_profiles
|
finished and custom installation steps for specific default_profiles
|
||||||
are needed
|
are needed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def provision(self, install_session: Installer, users: list[User]) -> None:
|
||||||
|
"""
|
||||||
|
Hook that will be called when the installation process is
|
||||||
|
finished and user configuration for specific default_profiles
|
||||||
|
is needed
|
||||||
|
"""
|
||||||
|
|
||||||
def json(self) -> dict[str, str]:
|
def json(self) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Returns a json representation of the profile
|
Returns a json representation of the profile
|
||||||
"""
|
"""
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def do_on_select(self) -> SelectResult | None:
|
async def do_on_select(self) -> SelectResult | None:
|
||||||
"""
|
"""
|
||||||
Hook that will be called when a profile is selected
|
Hook that will be called when a profile is selected
|
||||||
"""
|
"""
|
||||||
return SelectResult.NewSelection
|
return SelectResult.NewSelection
|
||||||
|
|
||||||
def set_custom_settings(self, settings: dict[str, str | None]) -> None:
|
def set_custom_settings(self, settings: dict[CustomSetting, str | None]) -> None:
|
||||||
"""
|
"""
|
||||||
Set the custom settings for the profile.
|
Set the custom settings for the profile.
|
||||||
This is also called when the settings are parsed from the config
|
This is also called when the settings are parsed from the config
|
||||||
|
|
@ -151,19 +155,16 @@ class Profile:
|
||||||
return self.profile_type in top_levels
|
return self.profile_type in top_levels
|
||||||
|
|
||||||
def is_desktop_profile(self) -> bool:
|
def is_desktop_profile(self) -> bool:
|
||||||
return self.profile_type == ProfileType.Desktop if self._advanced_check() else False
|
return self.profile_type == ProfileType.Desktop
|
||||||
|
|
||||||
def is_server_type_profile(self) -> bool:
|
def is_server_type_profile(self) -> bool:
|
||||||
return self.profile_type == ProfileType.ServerType
|
return self.profile_type == ProfileType.ServerType
|
||||||
|
|
||||||
def is_desktop_type_profile(self) -> bool:
|
def is_desktop_type_profile(self) -> bool:
|
||||||
return (self.profile_type == ProfileType.DesktopEnv or self.profile_type == ProfileType.WindowMgr) if self._advanced_check() else False
|
return self.profile_type == ProfileType.DesktopEnv or self.profile_type == ProfileType.WindowMgr
|
||||||
|
|
||||||
def is_xorg_type_profile(self) -> bool:
|
def is_xorg_type_profile(self) -> bool:
|
||||||
return self.profile_type == ProfileType.Xorg if self._advanced_check() else False
|
return self.profile_type == ProfileType.Xorg
|
||||||
|
|
||||||
def is_tailored(self) -> bool:
|
|
||||||
return self.profile_type == ProfileType.Tailored
|
|
||||||
|
|
||||||
def is_custom_type_profile(self) -> bool:
|
def is_custom_type_profile(self) -> bool:
|
||||||
return self.profile_type == ProfileType.CustomType
|
return self.profile_type == ProfileType.CustomType
|
||||||
|
|
@ -179,10 +180,23 @@ class Profile:
|
||||||
def is_greeter_supported(self) -> bool:
|
def is_greeter_supported(self) -> bool:
|
||||||
return self._support_greeter
|
return self._support_greeter
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_server(self) -> DisplayServerType | None:
|
||||||
|
return self._display_server
|
||||||
|
|
||||||
def preview_text(self) -> str:
|
def preview_text(self) -> str:
|
||||||
"""
|
"""
|
||||||
Override this method to provide a preview text for the profile
|
Override this method to provide a preview text for the profile
|
||||||
"""
|
"""
|
||||||
|
if self.is_desktop_type_profile():
|
||||||
|
if self._display_server:
|
||||||
|
text = tr('Environment type: {} {}').format(self._display_server.value, self.profile_type.value)
|
||||||
|
else:
|
||||||
|
text = tr('Environment type: {}').format(self.profile_type.value)
|
||||||
|
if packages := self.packages_text():
|
||||||
|
text += f'\n{packages}'
|
||||||
|
return text
|
||||||
|
|
||||||
return self.packages_text()
|
return self.packages_text()
|
||||||
|
|
||||||
def packages_text(self, include_sub_packages: bool = False) -> str:
|
def packages_text(self, include_sub_packages: bool = False) -> str:
|
||||||
|
|
@ -199,6 +213,6 @@ class Profile:
|
||||||
text = tr('Installed packages') + ':\n'
|
text = tr('Installed packages') + ':\n'
|
||||||
|
|
||||||
for pkg in sorted(packages):
|
for pkg in sorted(packages):
|
||||||
text += f'\t- {pkg}\n'
|
text += f' - {pkg}\n'
|
||||||
|
|
||||||
return text
|
return text
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
from typing import TYPE_CHECKING, override
|
from typing import TYPE_CHECKING, Self, override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult
|
from archinstall.default_profiles.profile import Profile, ProfileType, SelectResult
|
||||||
from archinstall.lib.output import info
|
from archinstall.lib.log import info
|
||||||
|
from archinstall.lib.menu.helpers import Selection
|
||||||
from archinstall.lib.profile.profiles_handler import profile_handler
|
from archinstall.lib.profile.profiles_handler import profile_handler
|
||||||
from archinstall.tui.curses_menu import SelectMenu
|
|
||||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||||
from archinstall.tui.result import ResultType
|
from archinstall.tui.result import ResultType
|
||||||
from archinstall.tui.types import FrameProperties, PreviewStyle
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from archinstall.lib.installer import Installer
|
from archinstall.lib.installer import Installer
|
||||||
|
from archinstall.lib.models.users import User
|
||||||
|
|
||||||
|
|
||||||
class ServerProfile(Profile):
|
class ServerProfile(Profile):
|
||||||
def __init__(self, current_value: list[Profile] = []):
|
def __init__(self, current_value: list[Self] = []):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
'Server',
|
'Server',
|
||||||
ProfileType.Server,
|
ProfileType.Server,
|
||||||
|
|
@ -21,12 +21,12 @@ class ServerProfile(Profile):
|
||||||
)
|
)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def do_on_select(self) -> SelectResult:
|
async def do_on_select(self) -> SelectResult:
|
||||||
items = [
|
items = [
|
||||||
MenuItem(
|
MenuItem(
|
||||||
p.name,
|
p.name,
|
||||||
value=p,
|
value=p,
|
||||||
preview_action=lambda x: x.value.preview_text(),
|
preview_action=lambda x: x.value.preview_text() if x.value else None,
|
||||||
)
|
)
|
||||||
for p in profile_handler.get_server_profiles()
|
for p in profile_handler.get_server_profiles()
|
||||||
]
|
]
|
||||||
|
|
@ -34,15 +34,13 @@ class ServerProfile(Profile):
|
||||||
group = MenuItemGroup(items, sort_items=True)
|
group = MenuItemGroup(items, sort_items=True)
|
||||||
group.set_selected_by_value(self.current_selection)
|
group.set_selected_by_value(self.current_selection)
|
||||||
|
|
||||||
result = SelectMenu[Profile](
|
result = await Selection[Self](
|
||||||
group,
|
group,
|
||||||
allow_reset=True,
|
allow_reset=True,
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
preview_style=PreviewStyle.RIGHT,
|
|
||||||
preview_size='auto',
|
|
||||||
preview_frame=FrameProperties.max('Info'),
|
|
||||||
multi=True,
|
multi=True,
|
||||||
).run()
|
preview_location='right',
|
||||||
|
).show()
|
||||||
|
|
||||||
match result.type_:
|
match result.type_:
|
||||||
case ResultType.Selection:
|
case ResultType.Selection:
|
||||||
|
|
@ -55,12 +53,17 @@ class ServerProfile(Profile):
|
||||||
return SelectResult.ResetCurrent
|
return SelectResult.ResetCurrent
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def post_install(self, install_session: 'Installer') -> None:
|
def provision(self, install_session: Installer, users: list[User]) -> None:
|
||||||
|
for profile in self.current_selection:
|
||||||
|
profile.provision(install_session, users)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def post_install(self, install_session: Installer) -> None:
|
||||||
for profile in self.current_selection:
|
for profile in self.current_selection:
|
||||||
profile.post_install(install_session)
|
profile.post_install(install_session)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def install(self, install_session: 'Installer') -> None:
|
def install(self, install_session: Installer) -> None:
|
||||||
server_info = self.current_selection_names()
|
server_info = self.current_selection_names()
|
||||||
details = ', '.join(server_info)
|
details = ', '.join(server_info)
|
||||||
info(f'Now installing the selected servers: {details}')
|
info(f'Now installing the selected servers: {details}')
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from archinstall.default_profiles.profile import Profile, ProfileType
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from archinstall.lib.installer import Installer
|
from archinstall.lib.installer import Installer
|
||||||
|
from archinstall.lib.models.users import User
|
||||||
|
|
||||||
|
|
||||||
class DockerProfile(Profile):
|
class DockerProfile(Profile):
|
||||||
|
|
@ -24,9 +25,6 @@ class DockerProfile(Profile):
|
||||||
return ['docker']
|
return ['docker']
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def post_install(self, install_session: 'Installer') -> None:
|
def provision(self, install_session: Installer, users: list[User]) -> None:
|
||||||
from archinstall.lib.args import arch_config_handler
|
for user in users:
|
||||||
|
install_session.arch_chroot(f'usermod -a -G docker {user.username}')
|
||||||
if auth_config := arch_config_handler.config.auth_config:
|
|
||||||
for user in auth_config.users:
|
|
||||||
install_session.arch_chroot(f'usermod -a -G docker {user.username}')
|
|
||||||
|
|
|
||||||
|
|
@ -24,5 +24,5 @@ class MariadbProfile(Profile):
|
||||||
return ['mariadb']
|
return ['mariadb']
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def post_install(self, install_session: 'Installer') -> None:
|
def post_install(self, install_session: Installer) -> None:
|
||||||
install_session.arch_chroot('mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql')
|
install_session.arch_chroot('mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql')
|
||||||
|
|
|
||||||
|
|
@ -24,5 +24,5 @@ class PostgresqlProfile(Profile):
|
||||||
return ['postgresql']
|
return ['postgresql']
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def post_install(self, install_session: 'Installer') -> None:
|
def post_install(self, install_session: Installer) -> None:
|
||||||
install_session.arch_chroot('initdb -D /var/lib/postgres/data', run_as='postgres')
|
install_session.arch_chroot('initdb -D /var/lib/postgres/data', run_as='postgres')
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
from typing import TYPE_CHECKING, override
|
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import ProfileType
|
|
||||||
from archinstall.default_profiles.xorg import XorgProfile
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from archinstall.lib.installer import Installer
|
|
||||||
|
|
||||||
|
|
||||||
class TailoredProfile(XorgProfile):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__('52-54-00-12-34-56', ProfileType.Tailored)
|
|
||||||
|
|
||||||
@property
|
|
||||||
@override
|
|
||||||
def packages(self) -> list[str]:
|
|
||||||
return ['nano', 'wget', 'git']
|
|
||||||
|
|
||||||
@override
|
|
||||||
def install(self, install_session: 'Installer') -> None:
|
|
||||||
super().install(install_session)
|
|
||||||
# do whatever you like here :)
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
from typing import override
|
from typing import TYPE_CHECKING, override
|
||||||
|
|
||||||
from archinstall.default_profiles.profile import Profile, ProfileType
|
from archinstall.default_profiles.profile import DisplayServerType, Profile, ProfileType
|
||||||
from archinstall.lib.translationhandler import tr
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from archinstall.lib.installer import Installer
|
||||||
|
|
||||||
|
|
||||||
class XorgProfile(Profile):
|
class XorgProfile(Profile):
|
||||||
|
|
@ -9,26 +11,22 @@ class XorgProfile(Profile):
|
||||||
self,
|
self,
|
||||||
name: str = 'Xorg',
|
name: str = 'Xorg',
|
||||||
profile_type: ProfileType = ProfileType.Xorg,
|
profile_type: ProfileType = ProfileType.Xorg,
|
||||||
advanced: bool = False,
|
|
||||||
):
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name,
|
name,
|
||||||
profile_type,
|
profile_type,
|
||||||
support_gfx_driver=True,
|
support_gfx_driver=True,
|
||||||
advanced=advanced,
|
display_server=DisplayServerType.Xorg,
|
||||||
)
|
)
|
||||||
|
|
||||||
@override
|
|
||||||
def preview_text(self) -> str:
|
|
||||||
text = tr('Environment type: {}').format(self.profile_type.value)
|
|
||||||
if packages := self.packages_text():
|
|
||||||
text += f'\n{packages}'
|
|
||||||
|
|
||||||
return text
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def packages(self) -> list[str]:
|
def packages(self) -> list[str]:
|
||||||
return [
|
return [
|
||||||
'xorg-server',
|
'xorg-server',
|
||||||
|
'xorg-xinit',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@override
|
||||||
|
def install(self, install_session: Installer) -> None:
|
||||||
|
install_session.add_additional_packages(self.packages)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@ from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from archinstall.applications.audio import AudioApp
|
from archinstall.applications.audio import AudioApp
|
||||||
from archinstall.applications.bluetooth import BluetoothApp
|
from archinstall.applications.bluetooth import BluetoothApp
|
||||||
|
from archinstall.applications.firewall import FirewallApp
|
||||||
|
from archinstall.applications.fonts import FontsApp
|
||||||
|
from archinstall.applications.power_management import PowerManagementApp
|
||||||
|
from archinstall.applications.print_service import PrintServiceApp
|
||||||
|
from archinstall.lib.models import Audio
|
||||||
from archinstall.lib.models.application import ApplicationConfiguration
|
from archinstall.lib.models.application import ApplicationConfiguration
|
||||||
from archinstall.lib.models.users import User
|
from archinstall.lib.models.users import User
|
||||||
|
|
||||||
|
|
@ -13,16 +18,34 @@ class ApplicationHandler:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def install_applications(self, install_session: 'Installer', app_config: ApplicationConfiguration, users: list['User'] | None = None) -> None:
|
def install_applications(self, install_session: Installer, app_config: ApplicationConfiguration, users: list[User] | None = None) -> None:
|
||||||
if app_config.bluetooth_config:
|
if app_config.bluetooth_config and app_config.bluetooth_config.enabled:
|
||||||
BluetoothApp().install(install_session)
|
BluetoothApp().install(install_session)
|
||||||
|
|
||||||
if app_config.audio_config:
|
if app_config.audio_config and app_config.audio_config.audio != Audio.NO_AUDIO:
|
||||||
AudioApp().install(
|
AudioApp().install(
|
||||||
install_session,
|
install_session,
|
||||||
app_config.audio_config,
|
app_config.audio_config,
|
||||||
users,
|
users,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if app_config.power_management_config:
|
||||||
|
PowerManagementApp().install(
|
||||||
|
install_session,
|
||||||
|
app_config.power_management_config,
|
||||||
|
)
|
||||||
|
|
||||||
application_handler = ApplicationHandler()
|
if app_config.print_service_config and app_config.print_service_config.enabled:
|
||||||
|
PrintServiceApp().install(install_session)
|
||||||
|
|
||||||
|
if app_config.firewall_config:
|
||||||
|
FirewallApp().install(
|
||||||
|
install_session,
|
||||||
|
app_config.firewall_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
if app_config.fonts_config:
|
||||||
|
FontsApp().install(
|
||||||
|
install_session,
|
||||||
|
app_config.fonts_config,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,24 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
|
from archinstall.lib.hardware import SysInfo
|
||||||
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
|
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
|
||||||
from archinstall.lib.models.application import ApplicationConfiguration, Audio, AudioConfiguration, BluetoothConfiguration
|
from archinstall.lib.menu.helpers import Confirmation, Selection
|
||||||
|
from archinstall.lib.models.application import (
|
||||||
|
ApplicationConfiguration,
|
||||||
|
Audio,
|
||||||
|
AudioConfiguration,
|
||||||
|
BluetoothConfiguration,
|
||||||
|
Firewall,
|
||||||
|
FirewallConfiguration,
|
||||||
|
FontPackage,
|
||||||
|
FontsConfiguration,
|
||||||
|
PowerManagement,
|
||||||
|
PowerManagementConfiguration,
|
||||||
|
PrintServiceConfiguration,
|
||||||
|
)
|
||||||
from archinstall.lib.translationhandler import tr
|
from archinstall.lib.translationhandler import tr
|
||||||
from archinstall.tui.curses_menu import SelectMenu
|
|
||||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||||
from archinstall.tui.result import ResultType
|
from archinstall.tui.result import ResultType
|
||||||
from archinstall.tui.types import Alignment, FrameProperties, Orientation
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
|
class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
|
||||||
|
|
@ -19,8 +31,8 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
|
||||||
else:
|
else:
|
||||||
self._app_config = ApplicationConfiguration()
|
self._app_config = ApplicationConfiguration()
|
||||||
|
|
||||||
menu_optioons = self._define_menu_options()
|
menu_options = self._define_menu_options()
|
||||||
self._item_group = MenuItemGroup(menu_optioons, checkmarks=True)
|
self._item_group = MenuItemGroup(menu_options, checkmarks=True)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
self._item_group,
|
self._item_group,
|
||||||
|
|
@ -29,8 +41,8 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
|
||||||
)
|
)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def run(self, additional_title: str | None = None) -> ApplicationConfiguration:
|
async def show(self) -> ApplicationConfiguration | None:
|
||||||
super().run(additional_title=additional_title)
|
_ = await super().show()
|
||||||
return self._app_config
|
return self._app_config
|
||||||
|
|
||||||
def _define_menu_options(self) -> list[MenuItem]:
|
def _define_menu_options(self) -> list[MenuItem]:
|
||||||
|
|
@ -48,13 +60,45 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
|
||||||
preview_action=self._prev_audio,
|
preview_action=self._prev_audio,
|
||||||
key='audio_config',
|
key='audio_config',
|
||||||
),
|
),
|
||||||
|
MenuItem(
|
||||||
|
text=tr('Print service'),
|
||||||
|
action=select_print_service,
|
||||||
|
preview_action=self._prev_print_service,
|
||||||
|
key='print_service_config',
|
||||||
|
),
|
||||||
|
MenuItem(
|
||||||
|
text=tr('Power management'),
|
||||||
|
action=select_power_management,
|
||||||
|
preview_action=self._prev_power_management,
|
||||||
|
enabled=SysInfo.has_battery(),
|
||||||
|
key='power_management_config',
|
||||||
|
),
|
||||||
|
MenuItem(
|
||||||
|
text=tr('Firewall'),
|
||||||
|
action=select_firewall,
|
||||||
|
preview_action=self._prev_firewall,
|
||||||
|
key='firewall_config',
|
||||||
|
),
|
||||||
|
MenuItem(
|
||||||
|
text=tr('Additional fonts'),
|
||||||
|
action=select_fonts,
|
||||||
|
value=self._app_config.fonts_config,
|
||||||
|
preview_action=self._prev_fonts,
|
||||||
|
key='fonts_config',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def _prev_power_management(self, item: MenuItem) -> str | None:
|
||||||
|
if item.value is not None:
|
||||||
|
config: PowerManagementConfiguration = item.value
|
||||||
|
return f'{tr("Power management")}: {config.power_management.value}'
|
||||||
|
return None
|
||||||
|
|
||||||
def _prev_bluetooth(self, item: MenuItem) -> str | None:
|
def _prev_bluetooth(self, item: MenuItem) -> str | None:
|
||||||
if item.value is not None:
|
if item.value is not None:
|
||||||
bluetooth_config: BluetoothConfiguration = item.value
|
bluetooth_config: BluetoothConfiguration = item.value
|
||||||
|
|
||||||
output = 'Bluetooth: '
|
output = f'{tr("Bluetooth")}: '
|
||||||
output += tr('Enabled') if bluetooth_config.enabled else tr('Disabled')
|
output += tr('Enabled') if bluetooth_config.enabled else tr('Disabled')
|
||||||
return output
|
return output
|
||||||
return None
|
return None
|
||||||
|
|
@ -65,48 +109,101 @@ class ApplicationMenu(AbstractSubMenu[ApplicationConfiguration]):
|
||||||
return f'{tr("Audio")}: {config.audio.value}'
|
return f'{tr("Audio")}: {config.audio.value}'
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _prev_print_service(self, item: MenuItem) -> str | None:
|
||||||
|
if item.value is not None:
|
||||||
|
print_service_config: PrintServiceConfiguration = item.value
|
||||||
|
|
||||||
def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfiguration | None:
|
output = f'{tr("Print service")}: '
|
||||||
group = MenuItemGroup.yes_no()
|
output += tr('Enabled') if print_service_config.enabled else tr('Disabled')
|
||||||
group.focus_item = MenuItem.no()
|
return output
|
||||||
|
return None
|
||||||
|
|
||||||
if preset is not None:
|
def _prev_firewall(self, item: MenuItem) -> str | None:
|
||||||
group.set_selected_by_value(preset.enabled)
|
if item.value is not None:
|
||||||
|
config: FirewallConfiguration = item.value
|
||||||
|
return f'{tr("Firewall")}: {config.firewall.value}'
|
||||||
|
return None
|
||||||
|
|
||||||
header = tr('Would you like to configure Bluetooth?') + '\n'
|
def _prev_fonts(self, item: MenuItem) -> str | None:
|
||||||
|
if item.value is not None:
|
||||||
|
config: FontsConfiguration = item.value
|
||||||
|
packages = ', '.join(f.value for f in config.fonts)
|
||||||
|
return f'{tr("Additional fonts")}: {packages}'
|
||||||
|
return None
|
||||||
|
|
||||||
result = SelectMenu[bool](
|
|
||||||
|
async def select_power_management(preset: PowerManagementConfiguration | None = None) -> PowerManagementConfiguration | None:
|
||||||
|
group = MenuItemGroup.from_enum(PowerManagement)
|
||||||
|
|
||||||
|
if preset:
|
||||||
|
group.set_focus_by_value(preset.power_management)
|
||||||
|
|
||||||
|
result = await Selection[PowerManagement](
|
||||||
group,
|
group,
|
||||||
header=header,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
columns=2,
|
|
||||||
orientation=Orientation.HORIZONTAL,
|
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
).run()
|
allow_reset=True,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case ResultType.Selection:
|
||||||
|
return PowerManagementConfiguration(power_management=result.get_value())
|
||||||
|
case ResultType.Reset:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfiguration | None:
|
||||||
|
header = tr('Would you like to configure Bluetooth?') + '\n'
|
||||||
|
preset_val = preset.enabled if preset else False
|
||||||
|
|
||||||
|
result = await Confirmation(
|
||||||
|
header=header,
|
||||||
|
allow_skip=True,
|
||||||
|
preset=preset_val,
|
||||||
|
).show()
|
||||||
|
|
||||||
match result.type_:
|
match result.type_:
|
||||||
case ResultType.Selection:
|
case ResultType.Selection:
|
||||||
enabled = result.item() == MenuItem.yes()
|
return BluetoothConfiguration(result.get_value())
|
||||||
return BluetoothConfiguration(enabled)
|
|
||||||
case ResultType.Skip:
|
case ResultType.Skip:
|
||||||
return preset
|
return preset
|
||||||
case _:
|
case _:
|
||||||
raise ValueError('Unhandled result type')
|
raise ValueError('Unhandled result type')
|
||||||
|
|
||||||
|
|
||||||
def select_audio(preset: AudioConfiguration | None = None) -> AudioConfiguration | None:
|
async def select_print_service(preset: PrintServiceConfiguration | None) -> PrintServiceConfiguration | None:
|
||||||
|
header = tr('Would you like to configure the print service?') + '\n'
|
||||||
|
preset_val = preset.enabled if preset else False
|
||||||
|
|
||||||
|
result = await Confirmation(
|
||||||
|
header=header,
|
||||||
|
allow_skip=True,
|
||||||
|
preset=preset_val,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Selection:
|
||||||
|
result.get_value()
|
||||||
|
return PrintServiceConfiguration(result.get_value())
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case _:
|
||||||
|
raise ValueError('Unhandled result type')
|
||||||
|
|
||||||
|
|
||||||
|
async def select_audio(preset: AudioConfiguration | None = None) -> AudioConfiguration | None:
|
||||||
items = [MenuItem(a.value, value=a) for a in Audio]
|
items = [MenuItem(a.value, value=a) for a in Audio]
|
||||||
group = MenuItemGroup(items)
|
group = MenuItemGroup(items)
|
||||||
|
|
||||||
if preset:
|
if preset:
|
||||||
group.set_focus_by_value(preset.audio)
|
group.set_focus_by_value(preset.audio)
|
||||||
|
|
||||||
result = SelectMenu[Audio](
|
result = await Selection[Audio](
|
||||||
group,
|
group,
|
||||||
|
header=tr('Select audio configuration'),
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
alignment=Alignment.CENTER,
|
).show()
|
||||||
frame=FrameProperties.min(tr('Audio')),
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
match result.type_:
|
||||||
case ResultType.Skip:
|
case ResultType.Skip:
|
||||||
|
|
@ -115,3 +212,52 @@ def select_audio(preset: AudioConfiguration | None = None) -> AudioConfiguration
|
||||||
return AudioConfiguration(audio=result.get_value())
|
return AudioConfiguration(audio=result.get_value())
|
||||||
case ResultType.Reset:
|
case ResultType.Reset:
|
||||||
raise ValueError('Unhandled result type')
|
raise ValueError('Unhandled result type')
|
||||||
|
|
||||||
|
|
||||||
|
async def select_firewall(preset: FirewallConfiguration | None = None) -> FirewallConfiguration | None:
|
||||||
|
group = MenuItemGroup.from_enum(Firewall)
|
||||||
|
|
||||||
|
if preset:
|
||||||
|
group.set_focus_by_value(preset.firewall)
|
||||||
|
|
||||||
|
result = await Selection[Firewall](
|
||||||
|
group,
|
||||||
|
allow_skip=True,
|
||||||
|
allow_reset=True,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case ResultType.Selection:
|
||||||
|
return FirewallConfiguration(firewall=result.get_value())
|
||||||
|
case ResultType.Reset:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def select_fonts(preset: FontsConfiguration | None = None) -> FontsConfiguration | None:
|
||||||
|
items = [MenuItem(f'{f.value} ({f.description()})', value=f) for f in FontPackage]
|
||||||
|
group = MenuItemGroup(items)
|
||||||
|
|
||||||
|
if preset:
|
||||||
|
for f in preset.fonts:
|
||||||
|
group.set_selected_by_value(f)
|
||||||
|
|
||||||
|
result = await Selection[FontPackage](
|
||||||
|
group,
|
||||||
|
header=tr('Select font packages to install'),
|
||||||
|
allow_skip=True,
|
||||||
|
allow_reset=True,
|
||||||
|
multi=True,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case ResultType.Selection:
|
||||||
|
selected = result.get_values()
|
||||||
|
if selected:
|
||||||
|
return FontsConfiguration(fonts=selected)
|
||||||
|
return None
|
||||||
|
case ResultType.Reset:
|
||||||
|
return None
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,42 @@
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from argparse import ArgumentParser, Namespace
|
from argparse import ArgumentParser, Namespace
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from importlib.metadata import version
|
from enum import Enum, StrEnum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any, Self
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
from pydantic.dataclasses import dataclass as p_dataclass
|
from pydantic.dataclasses import dataclass as p_dataclass
|
||||||
|
|
||||||
from archinstall.lib.crypt import decrypt
|
from archinstall.lib.crypt import decrypt
|
||||||
from archinstall.lib.models.application import ApplicationConfiguration
|
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
|
from archinstall.lib.models.authentication import AuthenticationConfiguration
|
||||||
from archinstall.lib.models.bootloader import Bootloader
|
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
|
||||||
|
from archinstall.lib.models.config import SubConfig
|
||||||
from archinstall.lib.models.device import DiskEncryption, DiskLayoutConfiguration
|
from archinstall.lib.models.device import DiskEncryption, DiskLayoutConfiguration
|
||||||
from archinstall.lib.models.locale import LocaleConfiguration
|
from archinstall.lib.models.locale import LocaleConfiguration
|
||||||
from archinstall.lib.models.mirrors import MirrorConfiguration
|
from archinstall.lib.models.mirrors import MirrorConfiguration
|
||||||
from archinstall.lib.models.network import NetworkConfiguration
|
from archinstall.lib.models.network import NetworkConfiguration
|
||||||
|
from archinstall.lib.models.package_types import DEFAULT_KERNEL
|
||||||
from archinstall.lib.models.packages import Repository
|
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.profile import ProfileConfiguration
|
||||||
from archinstall.lib.models.users import Password, User, UserSerialization
|
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.plugins import load_plugin
|
||||||
from archinstall.lib.translationhandler import Language, tr, translation_handler
|
from archinstall.lib.translationhandler import Language, tr, translation_handler
|
||||||
from archinstall.lib.utils.util import get_password
|
from archinstall.lib.version import get_version
|
||||||
from archinstall.tui.curses_menu import Tui
|
from archinstall.tui.components import tui
|
||||||
|
|
||||||
|
|
||||||
|
class SubCommand(Enum):
|
||||||
|
SHARE_LOG = 'share-log'
|
||||||
|
|
||||||
|
|
||||||
@p_dataclass
|
@p_dataclass
|
||||||
|
|
@ -49,9 +58,87 @@ class Arguments:
|
||||||
no_pkg_lookups: bool = False
|
no_pkg_lookups: bool = False
|
||||||
plugin: str | None = None
|
plugin: str | None = None
|
||||||
skip_version_check: bool = False
|
skip_version_check: bool = False
|
||||||
|
skip_wifi_check: bool = False
|
||||||
advanced: bool = False
|
advanced: bool = False
|
||||||
verbose: bool = False
|
verbose: bool = False
|
||||||
|
|
||||||
|
command: SubCommand | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ArchConfigType(StrEnum):
|
||||||
|
VERSION = 'version'
|
||||||
|
SCRIPT = 'script'
|
||||||
|
LOCALE_CONFIG = 'locale_config'
|
||||||
|
ARCHINSTALL_LANGUAGE = 'archinstall_language'
|
||||||
|
DISK_CONFIG = 'disk_config'
|
||||||
|
PROFILE_CONFIG = 'profile_config'
|
||||||
|
MIRROR_CONFIG = 'mirror_config'
|
||||||
|
NETWORK_CONFIG = 'network_config'
|
||||||
|
BOOTLOADER_CONFIG = 'bootloader_config'
|
||||||
|
APP_CONFIG = 'app_config'
|
||||||
|
AUTH_CONFIG = 'auth_config'
|
||||||
|
SWAP = 'swap'
|
||||||
|
USERS = 'users'
|
||||||
|
ROOT_ENC_PASSWORD = 'root_enc_password'
|
||||||
|
ENCRYPTION_PASSWORD = 'encryption_password'
|
||||||
|
HOSTNAME = 'hostname'
|
||||||
|
KERNELS = 'kernels'
|
||||||
|
NTP = 'ntp'
|
||||||
|
TIMEZONE = 'timezone'
|
||||||
|
SERVICES = 'services'
|
||||||
|
PACKAGES = 'packages'
|
||||||
|
PACMAN_CONFIG = 'pacman_config'
|
||||||
|
CUSTOM_COMMANDS = 'custom_commands'
|
||||||
|
|
||||||
|
def text(self) -> str:
|
||||||
|
match self:
|
||||||
|
case ArchConfigType.ARCHINSTALL_LANGUAGE:
|
||||||
|
return tr('ArchInstall Language')
|
||||||
|
case ArchConfigType.VERSION:
|
||||||
|
return tr('Version')
|
||||||
|
case ArchConfigType.SCRIPT:
|
||||||
|
return tr('Installation Script')
|
||||||
|
case ArchConfigType.LOCALE_CONFIG:
|
||||||
|
return tr('Locales')
|
||||||
|
case ArchConfigType.DISK_CONFIG:
|
||||||
|
return tr('Disk configuration')
|
||||||
|
case ArchConfigType.PROFILE_CONFIG:
|
||||||
|
return tr('Profile')
|
||||||
|
case ArchConfigType.MIRROR_CONFIG:
|
||||||
|
return tr('Mirrors and repositories')
|
||||||
|
case ArchConfigType.NETWORK_CONFIG:
|
||||||
|
return tr('Network')
|
||||||
|
case ArchConfigType.BOOTLOADER_CONFIG:
|
||||||
|
return tr('Bootloader')
|
||||||
|
case ArchConfigType.APP_CONFIG:
|
||||||
|
return tr('Application')
|
||||||
|
case ArchConfigType.AUTH_CONFIG:
|
||||||
|
return tr('Authentication')
|
||||||
|
case ArchConfigType.SWAP:
|
||||||
|
return tr('Swap')
|
||||||
|
case ArchConfigType.HOSTNAME:
|
||||||
|
return tr('Hostname')
|
||||||
|
case ArchConfigType.KERNELS:
|
||||||
|
return tr('Kernels')
|
||||||
|
case ArchConfigType.NTP:
|
||||||
|
return tr('Automatic time sync (NTP)')
|
||||||
|
case ArchConfigType.TIMEZONE:
|
||||||
|
return tr('Timezone')
|
||||||
|
case ArchConfigType.SERVICES:
|
||||||
|
return tr('Services')
|
||||||
|
case ArchConfigType.PACKAGES:
|
||||||
|
return tr('Additional packages')
|
||||||
|
case ArchConfigType.PACMAN_CONFIG:
|
||||||
|
return tr('Pacman')
|
||||||
|
case ArchConfigType.CUSTOM_COMMANDS:
|
||||||
|
return tr('Custom commands')
|
||||||
|
case ArchConfigType.USERS:
|
||||||
|
return tr('Users')
|
||||||
|
case ArchConfigType.ROOT_ENC_PASSWORD:
|
||||||
|
return tr('Root encrypted password')
|
||||||
|
case ArchConfigType.ENCRYPTION_PASSWORD:
|
||||||
|
return tr('Disk encryption password')
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ArchConfig:
|
class ArchConfig:
|
||||||
|
|
@ -63,76 +150,101 @@ class ArchConfig:
|
||||||
profile_config: ProfileConfiguration | None = None
|
profile_config: ProfileConfiguration | None = None
|
||||||
mirror_config: MirrorConfiguration | None = None
|
mirror_config: MirrorConfiguration | None = None
|
||||||
network_config: NetworkConfiguration | None = None
|
network_config: NetworkConfiguration | None = None
|
||||||
bootloader: Bootloader | None = None
|
bootloader_config: BootloaderConfiguration | None = None
|
||||||
uki: bool = False
|
|
||||||
app_config: ApplicationConfiguration | None = None
|
app_config: ApplicationConfiguration | None = None
|
||||||
auth_config: AuthenticationConfiguration | None = None
|
auth_config: AuthenticationConfiguration | None = None
|
||||||
|
swap: ZramConfiguration | None = None
|
||||||
hostname: str = 'archlinux'
|
hostname: str = 'archlinux'
|
||||||
kernels: list[str] = field(default_factory=lambda: ['linux'])
|
kernels: list[str] = field(default_factory=lambda: [DEFAULT_KERNEL.value])
|
||||||
ntp: bool = True
|
ntp: bool = True
|
||||||
packages: list[str] = field(default_factory=list)
|
packages: list[str] = field(default_factory=list)
|
||||||
parallel_downloads: int = 0
|
pacman_config: PacmanConfiguration = field(default_factory=PacmanConfiguration.default)
|
||||||
swap: bool = True
|
|
||||||
timezone: str = 'UTC'
|
timezone: str = 'UTC'
|
||||||
services: list[str] = field(default_factory=list)
|
services: list[str] = field(default_factory=list)
|
||||||
custom_commands: list[str] = field(default_factory=list)
|
custom_commands: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
def unsafe_json(self) -> dict[str, Any]:
|
def unsafe_config(self) -> dict[ArchConfigType, Any]:
|
||||||
config: dict[str, list[UserSerialization] | str | None] = {}
|
config: dict[ArchConfigType, list[UserSerialization] | str | None] = {}
|
||||||
|
|
||||||
if self.auth_config:
|
if self.auth_config:
|
||||||
if self.auth_config.users:
|
if self.auth_config.users:
|
||||||
config['users'] = [user.json() for user in self.auth_config.users]
|
config[ArchConfigType.USERS] = [user.json() for user in self.auth_config.users]
|
||||||
|
|
||||||
if self.auth_config.root_enc_password:
|
if self.auth_config.root_enc_password:
|
||||||
config['root_enc_password'] = self.auth_config.root_enc_password.enc_password
|
config[ArchConfigType.ROOT_ENC_PASSWORD] = self.auth_config.root_enc_password.enc_password
|
||||||
|
|
||||||
if self.disk_config:
|
if self.disk_config:
|
||||||
disk_encryption = self.disk_config.disk_encryption
|
disk_encryption = self.disk_config.disk_encryption
|
||||||
if disk_encryption and disk_encryption.encryption_password:
|
if disk_encryption and disk_encryption.encryption_password:
|
||||||
config['encryption_password'] = disk_encryption.encryption_password.plaintext
|
config[ArchConfigType.ENCRYPTION_PASSWORD] = disk_encryption.encryption_password.plaintext
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def safe_json(self) -> dict[str, Any]:
|
def safe_config(self) -> dict[ArchConfigType, Any]:
|
||||||
config: Any = {
|
base_config: dict[ArchConfigType, Any] = {
|
||||||
'version': self.version,
|
ArchConfigType.VERSION: self.version,
|
||||||
'script': self.script,
|
ArchConfigType.SCRIPT: self.script,
|
||||||
'archinstall-language': self.archinstall_language.json(),
|
ArchConfigType.ARCHINSTALL_LANGUAGE: self.archinstall_language.json(),
|
||||||
'hostname': self.hostname,
|
|
||||||
'kernels': self.kernels,
|
|
||||||
'ntp': self.ntp,
|
|
||||||
'packages': self.packages,
|
|
||||||
'parallel_downloads': self.parallel_downloads,
|
|
||||||
'swap': self.swap,
|
|
||||||
'timezone': self.timezone,
|
|
||||||
'services': self.services,
|
|
||||||
'custom_commands': self.custom_commands,
|
|
||||||
'bootloader': self.bootloader.json() if self.bootloader else None,
|
|
||||||
'app_config': self.app_config.json() if self.app_config else None,
|
|
||||||
'auth_config': self.auth_config.json() if self.auth_config else None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.locale_config:
|
base_config.update(self.plain_cfg())
|
||||||
config['locale_config'] = self.locale_config.json()
|
sub_config = self.sub_cfg()
|
||||||
|
|
||||||
if self.disk_config:
|
for config_type, value in sub_config.items():
|
||||||
config['disk_config'] = self.disk_config.json()
|
if not hasattr(value, 'json'):
|
||||||
|
raise ValueError(f'Config value for {config_type} must implement json() method')
|
||||||
|
base_config[config_type] = value.json()
|
||||||
|
|
||||||
if self.profile_config:
|
return base_config
|
||||||
config['profile_config'] = self.profile_config.json()
|
|
||||||
|
def plain_cfg(self) -> dict[ArchConfigType, str | list[str] | bool]:
|
||||||
|
return {
|
||||||
|
ArchConfigType.HOSTNAME: self.hostname,
|
||||||
|
ArchConfigType.KERNELS: self.kernels,
|
||||||
|
ArchConfigType.NTP: self.ntp,
|
||||||
|
ArchConfigType.TIMEZONE: self.timezone,
|
||||||
|
ArchConfigType.SERVICES: self.services,
|
||||||
|
ArchConfigType.PACKAGES: self.packages,
|
||||||
|
ArchConfigType.CUSTOM_COMMANDS: self.custom_commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
def sub_cfg(self) -> dict[ArchConfigType, SubConfig]:
|
||||||
|
cfg: dict[ArchConfigType, SubConfig] = {
|
||||||
|
ArchConfigType.PACMAN_CONFIG: self.pacman_config,
|
||||||
|
}
|
||||||
|
|
||||||
if self.mirror_config:
|
if self.mirror_config:
|
||||||
config['mirror_config'] = self.mirror_config.json()
|
cfg[ArchConfigType.MIRROR_CONFIG] = self.mirror_config
|
||||||
|
|
||||||
|
if self.bootloader_config:
|
||||||
|
cfg[ArchConfigType.BOOTLOADER_CONFIG] = self.bootloader_config
|
||||||
|
|
||||||
|
if self.disk_config:
|
||||||
|
cfg[ArchConfigType.DISK_CONFIG] = self.disk_config
|
||||||
|
|
||||||
|
if self.swap:
|
||||||
|
cfg[ArchConfigType.SWAP] = self.swap
|
||||||
|
|
||||||
|
if self.auth_config:
|
||||||
|
cfg[ArchConfigType.AUTH_CONFIG] = self.auth_config
|
||||||
|
|
||||||
|
if self.locale_config:
|
||||||
|
cfg[ArchConfigType.LOCALE_CONFIG] = self.locale_config
|
||||||
|
|
||||||
|
if self.profile_config:
|
||||||
|
cfg[ArchConfigType.PROFILE_CONFIG] = self.profile_config
|
||||||
|
|
||||||
if self.network_config:
|
if self.network_config:
|
||||||
config['network_config'] = self.network_config.json()
|
cfg[ArchConfigType.NETWORK_CONFIG] = self.network_config
|
||||||
|
|
||||||
return config
|
if self.app_config:
|
||||||
|
cfg[ArchConfigType.APP_CONFIG] = self.app_config
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_config(cls, args_config: dict[str, Any], args: Arguments) -> 'ArchConfig':
|
def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self:
|
||||||
arch_config = ArchConfig()
|
arch_config = cls()
|
||||||
|
|
||||||
arch_config.locale_config = LocaleConfiguration.parse_arg(args_config)
|
arch_config.locale_config = LocaleConfiguration.parse_arg(args_config)
|
||||||
|
|
||||||
|
|
@ -141,6 +253,7 @@ class ArchConfig:
|
||||||
|
|
||||||
if archinstall_lang := args_config.get('archinstall-language', None):
|
if archinstall_lang := args_config.get('archinstall-language', None):
|
||||||
arch_config.archinstall_language = translation_handler.get_language_by_name(archinstall_lang)
|
arch_config.archinstall_language = translation_handler.get_language_by_name(archinstall_lang)
|
||||||
|
translation_handler.activate(arch_config.archinstall_language, set_font=False)
|
||||||
|
|
||||||
if disk_config := args_config.get('disk_config', {}):
|
if disk_config := args_config.get('disk_config', {}):
|
||||||
enc_password = args_config.get('encryption_password', '')
|
enc_password = args_config.get('encryption_password', '')
|
||||||
|
|
@ -177,11 +290,15 @@ class ArchConfig:
|
||||||
if net_config := args_config.get('network_config', None):
|
if net_config := args_config.get('network_config', None):
|
||||||
arch_config.network_config = NetworkConfiguration.parse_arg(net_config)
|
arch_config.network_config = NetworkConfiguration.parse_arg(net_config)
|
||||||
|
|
||||||
if bootloader_config := args_config.get('bootloader', None):
|
if bootloader_config_dict := args_config.get('bootloader_config', None):
|
||||||
arch_config.bootloader = Bootloader.from_arg(bootloader_config, args.skip_boot)
|
arch_config.bootloader_config = BootloaderConfiguration.parse_arg(bootloader_config_dict, args.skip_boot)
|
||||||
|
# DEPRECATED: separate bootloader and uki fields (backward compatibility)
|
||||||
if args_config.get('uki') and (arch_config.bootloader is None or not arch_config.bootloader.has_uki_support()):
|
elif bootloader_str := args_config.get('bootloader', None):
|
||||||
arch_config.uki = False
|
bootloader = Bootloader.from_arg(bootloader_str, args.skip_boot)
|
||||||
|
uki = args_config.get('uki', False)
|
||||||
|
if uki and not bootloader.has_uki_support():
|
||||||
|
uki = False
|
||||||
|
arch_config.bootloader_config = BootloaderConfiguration(bootloader=bootloader, uki=uki, removable=True)
|
||||||
|
|
||||||
# deprecated: backwards compatibility
|
# deprecated: backwards compatibility
|
||||||
audio_config_args = args_config.get('audio_config', None)
|
audio_config_args = args_config.get('audio_config', None)
|
||||||
|
|
@ -204,10 +321,14 @@ class ArchConfig:
|
||||||
if packages := args_config.get('packages', []):
|
if packages := args_config.get('packages', []):
|
||||||
arch_config.packages = packages
|
arch_config.packages = packages
|
||||||
|
|
||||||
if parallel_downloads := args_config.get('parallel_downloads', 0):
|
if pacman_config := args_config.get('pacman_config', None):
|
||||||
arch_config.parallel_downloads = parallel_downloads
|
arch_config.pacman_config = PacmanConfiguration.parse_arg(pacman_config)
|
||||||
|
elif parallel_downloads := args_config.get('parallel_downloads', 0):
|
||||||
|
arch_config.pacman_config = PacmanConfiguration(parallel_downloads=int(parallel_downloads))
|
||||||
|
|
||||||
arch_config.swap = args_config.get('swap', True)
|
swap_arg = args_config.get('swap')
|
||||||
|
if swap_arg is not None:
|
||||||
|
arch_config.swap = ZramConfiguration.parse_arg(swap_arg)
|
||||||
|
|
||||||
if timezone := args_config.get('timezone', 'UTC'):
|
if timezone := args_config.get('timezone', 'UTC'):
|
||||||
arch_config.timezone = timezone
|
arch_config.timezone = timezone
|
||||||
|
|
@ -228,7 +349,7 @@ class ArchConfig:
|
||||||
arch_config.auth_config = AuthenticationConfiguration()
|
arch_config.auth_config = AuthenticationConfiguration()
|
||||||
arch_config.auth_config.root_enc_password = root_password
|
arch_config.auth_config.root_enc_password = root_password
|
||||||
|
|
||||||
# DEPRECATED: backwards copatibility
|
# DEPRECATED: backwards compatibility
|
||||||
users: list[User] = []
|
users: list[User] = []
|
||||||
if args_users := args_config.get('!users', None):
|
if args_users := args_config.get('!users', None):
|
||||||
users = User.parse_arguments(args_users)
|
users = User.parse_arguments(args_users)
|
||||||
|
|
@ -250,17 +371,17 @@ class ArchConfig:
|
||||||
class ArchConfigHandler:
|
class ArchConfigHandler:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._parser: ArgumentParser = self._define_arguments()
|
self._parser: ArgumentParser = self._define_arguments()
|
||||||
args: Arguments = self._parse_args()
|
self._add_sub_parsers()
|
||||||
self._args = args
|
|
||||||
|
|
||||||
|
self._args: Arguments = self._parse_args()
|
||||||
config = self._parse_config()
|
config = self._parse_config()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._config = ArchConfig.from_config(config, args)
|
self._config = ArchConfig.from_config(config, self._args)
|
||||||
self._config.version = self._get_version()
|
self._config.version = get_version()
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
warn(str(err))
|
warn(str(err))
|
||||||
exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self) -> ArchConfig:
|
def config(self) -> ArchConfig:
|
||||||
|
|
@ -282,20 +403,19 @@ class ArchConfigHandler:
|
||||||
def print_help(self) -> None:
|
def print_help(self) -> None:
|
||||||
self._parser.print_help()
|
self._parser.print_help()
|
||||||
|
|
||||||
def _get_version(self) -> str:
|
def _add_sub_parsers(self) -> None:
|
||||||
try:
|
subparsers = self._parser.add_subparsers(dest='command', help='Available subcommands')
|
||||||
return version('archinstall')
|
_ = subparsers.add_parser(SubCommand.SHARE_LOG.value, help='Upload log file to public server')
|
||||||
except Exception:
|
|
||||||
return 'Archinstall version not found'
|
|
||||||
|
|
||||||
def _define_arguments(self) -> ArgumentParser:
|
def _define_arguments(self) -> ArgumentParser:
|
||||||
parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-v',
|
'-v',
|
||||||
'--version',
|
'--version',
|
||||||
action='version',
|
action='version',
|
||||||
default=False,
|
default=False,
|
||||||
version='%(prog)s ' + self._get_version(),
|
version='%(prog)s ' + get_version(),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--config',
|
'--config',
|
||||||
|
|
@ -407,6 +527,12 @@ class ArchConfigHandler:
|
||||||
default=False,
|
default=False,
|
||||||
help='Skip the version check when running archinstall',
|
help='Skip the version check when running archinstall',
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--skip-wifi-check',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
help='Skip wifi check when running archinstall',
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--advanced',
|
'--advanced',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
|
|
@ -419,7 +545,6 @@ class ArchConfigHandler:
|
||||||
default=False,
|
default=False,
|
||||||
help='Enabled verbose options',
|
help='Enabled verbose options',
|
||||||
)
|
)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def _parse_args(self) -> Arguments:
|
def _parse_args(self) -> Arguments:
|
||||||
|
|
@ -480,37 +605,37 @@ class ArchConfigHandler:
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
if 'Invalid password' in str(err):
|
if 'Invalid password' in str(err):
|
||||||
error(tr('Incorrect credentials file decryption password'))
|
error(tr('Incorrect credentials file decryption password'))
|
||||||
exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
debug(f'Error decrypting credentials file: {err}')
|
debug(f'Error decrypting credentials file: {err}')
|
||||||
raise err from err
|
raise err from err
|
||||||
else:
|
else:
|
||||||
incorrect_password = False
|
header = tr('Enter credentials file decryption password')
|
||||||
|
wrong_pwd_text = tr('Incorrect password')
|
||||||
|
prompt = header
|
||||||
|
|
||||||
with Tui():
|
while True:
|
||||||
while True:
|
decryption_pwd: Password | None = tui.run(
|
||||||
header = tr('Incorrect password') if incorrect_password else None
|
lambda p=prompt: get_password( # type: ignore[misc]
|
||||||
|
header=p,
|
||||||
decryption_pwd = get_password(
|
|
||||||
text=tr('Credentials file decryption password'),
|
|
||||||
header=header,
|
|
||||||
allow_skip=False,
|
allow_skip=False,
|
||||||
skip_confirmation=True,
|
no_confirmation=True,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if not decryption_pwd:
|
if not decryption_pwd:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
creds_data = decrypt(creds_data, decryption_pwd.plaintext)
|
creds_data = decrypt(creds_data, decryption_pwd.plaintext)
|
||||||
break
|
break
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
if 'Invalid password' in str(err):
|
if 'Invalid password' in str(err):
|
||||||
debug('Incorrect credentials file decryption password')
|
debug('Incorrect credentials file decryption password')
|
||||||
incorrect_password = True
|
prompt = f'{header}' + f'\n\n{wrong_pwd_text}'
|
||||||
else:
|
else:
|
||||||
debug(f'Error decrypting credentials file: {err}')
|
debug(f'Error decrypting credentials file: {err}')
|
||||||
raise err from err
|
raise err from err
|
||||||
|
|
||||||
return json.loads(creds_data)
|
return json.loads(creds_data)
|
||||||
|
|
||||||
|
|
@ -525,12 +650,12 @@ class ArchConfigHandler:
|
||||||
else:
|
else:
|
||||||
error('Not a valid url')
|
error('Not a valid url')
|
||||||
|
|
||||||
exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def _read_file(self, path: Path) -> str:
|
def _read_file(self, path: Path) -> str:
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
error(f'Could not find file {path}')
|
error(f'Could not find file {path}')
|
||||||
exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return path.read_text()
|
return path.read_text()
|
||||||
|
|
||||||
|
|
@ -544,6 +669,3 @@ class ArchConfigHandler:
|
||||||
clean_args[key] = val
|
clean_args[key] = val
|
||||||
|
|
||||||
return clean_args
|
return clean_args
|
||||||
|
|
||||||
|
|
||||||
arch_config_handler: ArchConfigHandler = ArchConfigHandler()
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,11 @@ import getpass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from archinstall.lib.general import SysCommandWorker
|
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.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
|
||||||
from archinstall.lib.models.users import User
|
from archinstall.lib.models.users import User
|
||||||
from archinstall.lib.output import debug
|
|
||||||
from archinstall.lib.translationhandler import tr
|
from archinstall.lib.translationhandler import tr
|
||||||
from archinstall.tui.curses_menu import Tui
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from archinstall.lib.installer import Installer
|
from archinstall.lib.installer import Installer
|
||||||
|
|
@ -16,20 +15,20 @@ if TYPE_CHECKING:
|
||||||
class AuthenticationHandler:
|
class AuthenticationHandler:
|
||||||
def setup_auth(
|
def setup_auth(
|
||||||
self,
|
self,
|
||||||
install_session: 'Installer',
|
install_session: Installer,
|
||||||
auth_config: AuthenticationConfiguration,
|
auth_config: AuthenticationConfiguration,
|
||||||
hostname: str,
|
hostname: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
if auth_config.u2f_config and auth_config.users is not None:
|
if auth_config.u2f_config and auth_config.users is not None:
|
||||||
self._setup_u2f_login(install_session, auth_config.u2f_config, auth_config.users, hostname)
|
self._setup_u2f_login(install_session, auth_config.u2f_config, auth_config.users, hostname)
|
||||||
|
|
||||||
def _setup_u2f_login(self, install_session: 'Installer', u2f_config: U2FLoginConfiguration, users: list[User], hostname: str) -> None:
|
def _setup_u2f_login(self, install_session: Installer, u2f_config: U2FLoginConfiguration, users: list[User], hostname: str) -> None:
|
||||||
self._configure_u2f_mapping(install_session, u2f_config, users, hostname)
|
self._configure_u2f_mapping(install_session, u2f_config, users, hostname)
|
||||||
self._update_pam_config(install_session, u2f_config)
|
self._update_pam_config(install_session, u2f_config)
|
||||||
|
|
||||||
def _update_pam_config(
|
def _update_pam_config(
|
||||||
self,
|
self,
|
||||||
install_session: 'Installer',
|
install_session: Installer,
|
||||||
u2f_config: U2FLoginConfiguration,
|
u2f_config: U2FLoginConfiguration,
|
||||||
) -> None:
|
) -> None:
|
||||||
match u2f_config.u2f_login_method:
|
match u2f_config.u2f_login_method:
|
||||||
|
|
@ -54,7 +53,7 @@ class AuthenticationHandler:
|
||||||
def _add_u2f_entry(self, file: Path, entry: str) -> None:
|
def _add_u2f_entry(self, file: Path, entry: str) -> None:
|
||||||
if not file.exists():
|
if not file.exists():
|
||||||
debug(f'File does not exist: {file}')
|
debug(f'File does not exist: {file}')
|
||||||
return None
|
return
|
||||||
|
|
||||||
content = file.read_text().splitlines()
|
content = file.read_text().splitlines()
|
||||||
|
|
||||||
|
|
@ -73,7 +72,7 @@ class AuthenticationHandler:
|
||||||
|
|
||||||
def _configure_u2f_mapping(
|
def _configure_u2f_mapping(
|
||||||
self,
|
self,
|
||||||
install_session: 'Installer',
|
install_session: Installer,
|
||||||
u2f_config: U2FLoginConfiguration,
|
u2f_config: U2FLoginConfiguration,
|
||||||
users: list[User],
|
users: list[User],
|
||||||
hostname: str,
|
hostname: str,
|
||||||
|
|
@ -82,7 +81,7 @@ class AuthenticationHandler:
|
||||||
|
|
||||||
install_session.pacman.strap('pam-u2f')
|
install_session.pacman.strap('pam-u2f')
|
||||||
|
|
||||||
Tui.print(tr(f'Setting up U2F login: {u2f_config.u2f_login_method.value}'))
|
print(tr('Setting up U2F login: {}').format(u2f_config.u2f_login_method.value))
|
||||||
|
|
||||||
# https://developers.yubico.com/pam-u2f/
|
# https://developers.yubico.com/pam-u2f/
|
||||||
u2f_auth_file = install_session.target / 'etc/u2f_mappings'
|
u2f_auth_file = install_session.target / 'etc/u2f_mappings'
|
||||||
|
|
@ -92,11 +91,13 @@ class AuthenticationHandler:
|
||||||
registered_keys: list[str] = []
|
registered_keys: list[str] = []
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
Tui.print('')
|
print('')
|
||||||
Tui.print(tr('Setting up U2F device for user: {}').format(user.username))
|
info(tr('Setting up U2F device for user: {}').format(user.username))
|
||||||
Tui.print(tr('You may need to enter the PIN and then touch your U2F device to register it'))
|
info(tr('You may need to enter the PIN and then touch your U2F device to register it'))
|
||||||
|
|
||||||
cmd = ' '.join(['arch-chroot', str(install_session.target), 'pamu2fcfg', '-u', user.username, '-o', f'pam://{hostname}', '-i', f'pam://{hostname}'])
|
cmd = ' '.join(
|
||||||
|
['arch-chroot', '-S', str(install_session.target), 'pamu2fcfg', '-u', user.username, '-o', f'pam://{hostname}', '-i', f'pam://{hostname}']
|
||||||
|
)
|
||||||
|
|
||||||
debug(f'Enrolling U2F device: {cmd}')
|
debug(f'Enrolling U2F device: {cmd}')
|
||||||
|
|
||||||
|
|
@ -123,6 +124,3 @@ class AuthenticationHandler:
|
||||||
existing_keys = all_keys
|
existing_keys = all_keys
|
||||||
|
|
||||||
u2f_auth_file.write_text(existing_keys)
|
u2f_auth_file.write_text(existing_keys)
|
||||||
|
|
||||||
|
|
||||||
auth_handler = AuthenticationHandler()
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
from archinstall.lib.disk.fido import Fido2
|
from archinstall.lib.disk.fido import Fido2
|
||||||
from archinstall.lib.interactions.manage_users_conf import ask_for_additional_users
|
|
||||||
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
|
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
|
||||||
|
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.authentication import AuthenticationConfiguration, U2FLoginConfiguration, U2FLoginMethod
|
||||||
from archinstall.lib.models.users import Password, User
|
from archinstall.lib.models.users import Password, User
|
||||||
from archinstall.lib.output import FormattedOutput
|
|
||||||
from archinstall.lib.translationhandler import tr
|
from archinstall.lib.translationhandler import tr
|
||||||
from archinstall.lib.utils.util import get_password
|
from archinstall.lib.user.user_menu import select_users
|
||||||
from archinstall.tui.curses_menu import SelectMenu
|
from archinstall.lib.utils.format import as_table
|
||||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||||
from archinstall.tui.result import ResultType
|
from archinstall.tui.result import ResultType
|
||||||
from archinstall.tui.types import Alignment, FrameProperties, Orientation
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
|
class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
|
||||||
|
|
@ -21,8 +20,8 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
|
||||||
else:
|
else:
|
||||||
self._auth_config = AuthenticationConfiguration()
|
self._auth_config = AuthenticationConfiguration()
|
||||||
|
|
||||||
menu_optioons = self._define_menu_options()
|
menu_options = self._define_menu_options()
|
||||||
self._item_group = MenuItemGroup(menu_optioons, checkmarks=True)
|
self._item_group = MenuItemGroup(menu_options, checkmarks=True)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
self._item_group,
|
self._item_group,
|
||||||
|
|
@ -31,15 +30,14 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
|
||||||
)
|
)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def run(self, additional_title: str | None = None) -> AuthenticationConfiguration:
|
async def show(self) -> AuthenticationConfiguration | None:
|
||||||
super().run(additional_title=additional_title)
|
return await super().show()
|
||||||
return self._auth_config
|
|
||||||
|
|
||||||
def _define_menu_options(self) -> list[MenuItem]:
|
def _define_menu_options(self) -> list[MenuItem]:
|
||||||
return [
|
return [
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text=tr('Root password'),
|
text=tr('Root password'),
|
||||||
action=select_root_password,
|
action=lambda x: select_root_password(),
|
||||||
preview_action=self._prev_root_pwd,
|
preview_action=self._prev_root_pwd,
|
||||||
key='root_enc_password',
|
key='root_enc_password',
|
||||||
),
|
),
|
||||||
|
|
@ -58,16 +56,16 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def _create_user_account(self, preset: list[User] | None = None) -> list[User]:
|
async def _create_user_account(self, preset: list[User] | None = None) -> list[User]:
|
||||||
preset = [] if preset is None else preset
|
preset = [] if preset is None else preset
|
||||||
users = ask_for_additional_users(defined_users=preset)
|
users = await select_users(preset=preset)
|
||||||
return users
|
return users
|
||||||
|
|
||||||
def _prev_users(self, item: MenuItem) -> str | None:
|
def _prev_users(self, item: MenuItem) -> str | None:
|
||||||
users: list[User] | None = item.value
|
users: list[User] | None = item.value
|
||||||
|
|
||||||
if users:
|
if users:
|
||||||
return FormattedOutput.as_table(users)
|
return as_table(users)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _prev_root_pwd(self, item: MenuItem) -> str | None:
|
def _prev_root_pwd(self, item: MenuItem) -> str | None:
|
||||||
|
|
@ -101,12 +99,12 @@ class AuthenticationMenu(AbstractSubMenu[AuthenticationConfiguration]):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def select_root_password(preset: str | None = None) -> Password | None:
|
async def select_root_password() -> Password | None:
|
||||||
password = get_password(text=tr('Root password'), allow_skip=True)
|
password = await get_password(header=tr('Enter root password'), allow_skip=True)
|
||||||
return password
|
return password
|
||||||
|
|
||||||
|
|
||||||
def select_u2f_login(preset: U2FLoginConfiguration) -> U2FLoginConfiguration | None:
|
async def select_u2f_login(preset: U2FLoginConfiguration | None) -> U2FLoginConfiguration | None:
|
||||||
devices = Fido2.get_fido2_devices()
|
devices = Fido2.get_fido2_devices()
|
||||||
if not devices:
|
if not devices:
|
||||||
return None
|
return None
|
||||||
|
|
@ -120,30 +118,22 @@ def select_u2f_login(preset: U2FLoginConfiguration) -> U2FLoginConfiguration | N
|
||||||
if preset is not None:
|
if preset is not None:
|
||||||
group.set_selected_by_value(preset.u2f_login_method)
|
group.set_selected_by_value(preset.u2f_login_method)
|
||||||
|
|
||||||
result = SelectMenu[U2FLoginMethod](
|
result = await Selection[U2FLoginMethod](
|
||||||
group,
|
group,
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
frame=FrameProperties.min(tr('U2F Login Method')),
|
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
allow_reset=True,
|
allow_reset=True,
|
||||||
).run()
|
).show()
|
||||||
|
|
||||||
match result.type_:
|
match result.type_:
|
||||||
case ResultType.Selection:
|
case ResultType.Selection:
|
||||||
u2f_method = result.get_value()
|
u2f_method = result.get_value()
|
||||||
|
|
||||||
group = MenuItemGroup.yes_no()
|
|
||||||
group.focus_item = MenuItem.no()
|
|
||||||
header = tr('Enable passwordless sudo?')
|
header = tr('Enable passwordless sudo?')
|
||||||
|
|
||||||
result_sudo = SelectMenu[bool](
|
result_sudo = await Confirmation(
|
||||||
group,
|
|
||||||
header=header,
|
header=header,
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
columns=2,
|
|
||||||
orientation=Orientation.HORIZONTAL,
|
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
).run()
|
preset=False,
|
||||||
|
).show()
|
||||||
|
|
||||||
passwordless_sudo = result_sudo.item() == MenuItem.yes()
|
passwordless_sudo = result_sudo.item() == MenuItem.yes()
|
||||||
|
|
||||||
|
|
@ -155,5 +145,3 @@ def select_u2f_login(preset: U2FLoginConfiguration) -> U2FLoginConfiguration | N
|
||||||
return preset
|
return preset
|
||||||
case ResultType.Reset:
|
case ResultType.Reset:
|
||||||
return None
|
return None
|
||||||
case _:
|
|
||||||
raise ValueError('Unhandled result type')
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,33 @@
|
||||||
import time
|
import time
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
|
from pathlib import Path
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
|
from typing import ClassVar, Self
|
||||||
|
|
||||||
from .exceptions import SysCallError
|
from archinstall.lib.command import SysCommand, SysCommandWorker
|
||||||
from .general import SysCommand, SysCommandWorker, locate_binary
|
from archinstall.lib.exceptions import SysCallError
|
||||||
from .installer import Installer
|
from archinstall.lib.log import error
|
||||||
from .output import error
|
|
||||||
from .storage import storage
|
|
||||||
|
|
||||||
|
|
||||||
class Boot:
|
class Boot:
|
||||||
def __init__(self, installation: Installer):
|
_active_boot: ClassVar[Self | None] = None
|
||||||
self.instance = installation
|
|
||||||
|
def __init__(self, path: Path | str):
|
||||||
|
if isinstance(path, Path):
|
||||||
|
path = str(path)
|
||||||
|
|
||||||
|
self.path = path
|
||||||
self.container_name = 'archinstall'
|
self.container_name = 'archinstall'
|
||||||
self.session: SysCommandWorker | None = None
|
self.session: SysCommandWorker | None = None
|
||||||
self.ready = False
|
self.ready = False
|
||||||
|
|
||||||
def __enter__(self) -> 'Boot':
|
def __enter__(self) -> Self:
|
||||||
if (existing_session := storage.get('active_boot', None)) and existing_session.instance != self.instance:
|
if Boot._active_boot and Boot._active_boot.path != self.path:
|
||||||
raise KeyError('Archinstall only supports booting up one instance and another session is already active.')
|
raise KeyError('Archinstall only supports booting up one instance and another session is already active.')
|
||||||
|
|
||||||
if existing_session:
|
if Boot._active_boot:
|
||||||
self.session = existing_session.session
|
self.session = Boot._active_boot.session
|
||||||
self.ready = existing_session.ready
|
self.ready = Boot._active_boot.ready
|
||||||
else:
|
else:
|
||||||
# '-P' or --console=pipe could help us not having to do a bunch
|
# '-P' or --console=pipe could help us not having to do a bunch
|
||||||
# of os.write() calls, but instead use pipes (stdin, stdout and stderr) as usual.
|
# of os.write() calls, but instead use pipes (stdin, stdout and stderr) as usual.
|
||||||
|
|
@ -30,7 +35,7 @@ class Boot:
|
||||||
[
|
[
|
||||||
'systemd-nspawn',
|
'systemd-nspawn',
|
||||||
'-D',
|
'-D',
|
||||||
str(self.instance.target),
|
self.path,
|
||||||
'--timezone=off',
|
'--timezone=off',
|
||||||
'-b',
|
'-b',
|
||||||
'--no-pager',
|
'--no-pager',
|
||||||
|
|
@ -45,7 +50,7 @@ class Boot:
|
||||||
self.ready = True
|
self.ready = True
|
||||||
break
|
break
|
||||||
|
|
||||||
storage['active_boot'] = self
|
Boot._active_boot = self
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
|
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
|
||||||
|
|
@ -55,7 +60,7 @@ class Boot:
|
||||||
if exc_type is not None:
|
if exc_type is not None:
|
||||||
error(
|
error(
|
||||||
str(exc_value),
|
str(exc_value),
|
||||||
f'The error above occurred in a temporary boot-up of the installation {self.instance}',
|
f'The error above occurred in a temporary boot-up of the installation {self.path!r}',
|
||||||
)
|
)
|
||||||
|
|
||||||
shutdown = None
|
shutdown = None
|
||||||
|
|
@ -74,12 +79,12 @@ class Boot:
|
||||||
shutdown_exit_code = shutdown.exit_code
|
shutdown_exit_code = shutdown.exit_code
|
||||||
|
|
||||||
if self.session and (self.session.exit_code == 0 or shutdown_exit_code == 0):
|
if self.session and (self.session.exit_code == 0 or shutdown_exit_code == 0):
|
||||||
storage['active_boot'] = None
|
Boot._active_boot = None
|
||||||
else:
|
else:
|
||||||
session_exit_code = self.session.exit_code if self.session else -1
|
session_exit_code = self.session.exit_code if self.session else -1
|
||||||
|
|
||||||
raise SysCallError(
|
raise SysCallError(
|
||||||
f'Could not shut down temporary boot of {self.instance}: {session_exit_code}/{shutdown_exit_code}',
|
f'Could not shut down temporary boot of {self.path!r}: {session_exit_code}/{shutdown_exit_code}',
|
||||||
exit_code=next(filter(bool, [session_exit_code, shutdown_exit_code])),
|
exit_code=next(filter(bool, [session_exit_code, shutdown_exit_code])),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -100,17 +105,7 @@ class Boot:
|
||||||
return self.session.is_alive()
|
return self.session.is_alive()
|
||||||
|
|
||||||
def SysCommand(self, cmd: list[str], *args, **kwargs) -> SysCommand: # type: ignore[no-untyped-def]
|
def SysCommand(self, cmd: list[str], *args, **kwargs) -> SysCommand: # type: ignore[no-untyped-def]
|
||||||
if cmd[0][0] != '/' and cmd[0][:2] != './':
|
|
||||||
# This check is also done in SysCommand & SysCommandWorker.
|
|
||||||
# However, that check is done for `machinectl` and not for our chroot command.
|
|
||||||
# So this wrapper for SysCommand will do this additionally.
|
|
||||||
|
|
||||||
cmd[0] = locate_binary(cmd[0])
|
|
||||||
|
|
||||||
return SysCommand(['systemd-run', f'--machine={self.container_name}', '--pty', *cmd], *args, **kwargs)
|
return SysCommand(['systemd-run', f'--machine={self.container_name}', '--pty', *cmd], *args, **kwargs)
|
||||||
|
|
||||||
def SysCommandWorker(self, cmd: list[str], *args, **kwargs) -> SysCommandWorker: # type: ignore[no-untyped-def]
|
def SysCommandWorker(self, cmd: list[str], *args, **kwargs) -> SysCommandWorker: # type: ignore[no-untyped-def]
|
||||||
if cmd[0][0] != '/' and cmd[0][:2] != './':
|
|
||||||
cmd[0] = locate_binary(cmd[0])
|
|
||||||
|
|
||||||
return SysCommandWorker(['systemd-run', f'--machine={self.container_name}', '--pty', *cmd], *args, **kwargs)
|
return SysCommandWorker(['systemd-run', f'--machine={self.container_name}', '--pty', *cmd], *args, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
import textwrap
|
||||||
|
from typing import override
|
||||||
|
|
||||||
|
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
|
||||||
|
from archinstall.lib.menu.helpers import Confirmation, Selection
|
||||||
|
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
|
||||||
|
from archinstall.lib.translationhandler import tr
|
||||||
|
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||||
|
from archinstall.tui.result import ResultType
|
||||||
|
|
||||||
|
|
||||||
|
class BootloaderMenu(AbstractSubMenu[BootloaderConfiguration]):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
bootloader_conf: BootloaderConfiguration,
|
||||||
|
uefi: bool,
|
||||||
|
skip_boot: bool = False,
|
||||||
|
):
|
||||||
|
self._bootloader_conf = bootloader_conf
|
||||||
|
self._skip_boot = skip_boot
|
||||||
|
self._uefi = uefi
|
||||||
|
menu_options = self._define_menu_options()
|
||||||
|
|
||||||
|
self._item_group = MenuItemGroup(menu_options, sort_items=False, checkmarks=True)
|
||||||
|
super().__init__(
|
||||||
|
self._item_group,
|
||||||
|
config=self._bootloader_conf,
|
||||||
|
allow_reset=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _define_menu_options(self) -> list[MenuItem]:
|
||||||
|
bootloader = self._bootloader_conf.bootloader
|
||||||
|
|
||||||
|
# UKI availability
|
||||||
|
uki_enabled = self._uefi and bootloader.has_uki_support()
|
||||||
|
if not uki_enabled:
|
||||||
|
self._bootloader_conf.uki = False
|
||||||
|
|
||||||
|
# Removable availability
|
||||||
|
removable_enabled = self._uefi and bootloader.has_removable_support()
|
||||||
|
if not removable_enabled:
|
||||||
|
self._bootloader_conf.removable = False
|
||||||
|
|
||||||
|
return [
|
||||||
|
MenuItem(
|
||||||
|
text=tr('Bootloader'),
|
||||||
|
action=self._select_bootloader,
|
||||||
|
value=self._bootloader_conf.bootloader,
|
||||||
|
preview_action=self._prev_bootloader,
|
||||||
|
mandatory=True,
|
||||||
|
key='bootloader',
|
||||||
|
),
|
||||||
|
MenuItem(
|
||||||
|
text=tr('Unified kernel images'),
|
||||||
|
action=self._select_uki,
|
||||||
|
value=self._bootloader_conf.uki,
|
||||||
|
preview_action=self._prev_uki,
|
||||||
|
key='uki',
|
||||||
|
enabled=uki_enabled,
|
||||||
|
),
|
||||||
|
MenuItem(
|
||||||
|
text=tr('Install to removable location'),
|
||||||
|
action=self._select_removable,
|
||||||
|
value=self._bootloader_conf.removable,
|
||||||
|
preview_action=self._prev_removable,
|
||||||
|
key='removable',
|
||||||
|
enabled=removable_enabled,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def _prev_bootloader(self, item: MenuItem) -> str | None:
|
||||||
|
if item.value:
|
||||||
|
return f'{tr("Bootloader")}: {item.value.value}'
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _prev_uki(self, item: MenuItem) -> str | None:
|
||||||
|
uki_text = f'{tr("Unified kernel images")}'
|
||||||
|
if item.value:
|
||||||
|
return f'{uki_text}: {tr("Enabled")}'
|
||||||
|
else:
|
||||||
|
return f'{uki_text}: {tr("Disabled")}'
|
||||||
|
|
||||||
|
def _prev_removable(self, item: MenuItem) -> str | None:
|
||||||
|
if item.value:
|
||||||
|
return tr('Will install to /EFI/BOOT/ (removable location, safe default)')
|
||||||
|
return tr('Will install to custom location with NVRAM entry')
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def show(self) -> BootloaderConfiguration:
|
||||||
|
_ = await super().show()
|
||||||
|
return self._bootloader_conf
|
||||||
|
|
||||||
|
async def _select_bootloader(self, preset: Bootloader | None) -> Bootloader | None:
|
||||||
|
bootloader = await select_bootloader(preset, self._uefi, self._skip_boot)
|
||||||
|
|
||||||
|
if bootloader:
|
||||||
|
# Update UKI option based on bootloader
|
||||||
|
uki_item = self._menu_item_group.find_by_key('uki')
|
||||||
|
if not self._uefi or not bootloader.has_uki_support():
|
||||||
|
uki_item.enabled = False
|
||||||
|
uki_item.value = False
|
||||||
|
self._bootloader_conf.uki = False
|
||||||
|
else:
|
||||||
|
uki_item.enabled = True
|
||||||
|
|
||||||
|
# Update removable option based on bootloader
|
||||||
|
removable_item = self._menu_item_group.find_by_key('removable')
|
||||||
|
if not self._uefi or not bootloader.has_removable_support():
|
||||||
|
removable_item.enabled = False
|
||||||
|
removable_item.value = False
|
||||||
|
self._bootloader_conf.removable = False
|
||||||
|
else:
|
||||||
|
if not removable_item.enabled:
|
||||||
|
removable_item.value = True
|
||||||
|
self._bootloader_conf.removable = True
|
||||||
|
removable_item.enabled = True
|
||||||
|
|
||||||
|
return bootloader
|
||||||
|
|
||||||
|
async def _select_uki(self, preset: bool) -> bool:
|
||||||
|
prompt = tr('Would you like to use unified kernel images?') + '\n'
|
||||||
|
|
||||||
|
result = await Confirmation(header=prompt, allow_skip=True, preset=preset).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case ResultType.Selection:
|
||||||
|
return result.item() == MenuItem.yes()
|
||||||
|
case ResultType.Reset:
|
||||||
|
raise ValueError('Unhandled result type')
|
||||||
|
|
||||||
|
async def _select_removable(self, preset: bool) -> bool:
|
||||||
|
prompt = (
|
||||||
|
tr('Would you like to install the bootloader to the default removable media search location?')
|
||||||
|
+ '\n\n'
|
||||||
|
+ tr('This installs the bootloader to /EFI/BOOT/BOOTX64.EFI (or similar) which is useful for:')
|
||||||
|
+ '\n\n • '
|
||||||
|
+ tr('Firmware that does not properly support NVRAM boot entries like most MSI motherboards,')
|
||||||
|
+ '\n '
|
||||||
|
+ tr('most Apple Macs, many laptops...')
|
||||||
|
+ '\n • '
|
||||||
|
+ tr('USB drives or other portable external media.')
|
||||||
|
+ '\n • '
|
||||||
|
+ tr('Systems where you want the disk to be bootable on any computer.')
|
||||||
|
+ '\n\n'
|
||||||
|
+ tr(
|
||||||
|
textwrap.dedent(
|
||||||
|
"""\
|
||||||
|
If you do not know what this means, LEAVE THIS OPTION ENABLED, as it is the safe default.
|
||||||
|
|
||||||
|
It is suggested to disable this if none of the above apply, as it makes installing multiple
|
||||||
|
EFI bootloaders on the same disk easier, and it will not overwrite whatever bootloader
|
||||||
|
was previously installed at the default removable media search location, if any.
|
||||||
|
|
||||||
|
It may also make the installation more resilient in case of dual-booting with Windows,
|
||||||
|
as Windows is known to sometimes erase or replace the bootloader installed at the removable
|
||||||
|
location.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
+ '\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await Confirmation(
|
||||||
|
header=prompt,
|
||||||
|
allow_skip=True,
|
||||||
|
preset=preset,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case ResultType.Selection:
|
||||||
|
return result.get_value()
|
||||||
|
case ResultType.Reset:
|
||||||
|
raise ValueError('Unhandled result type')
|
||||||
|
|
||||||
|
|
||||||
|
async def select_bootloader(
|
||||||
|
preset: Bootloader | None,
|
||||||
|
uefi: bool,
|
||||||
|
skip_boot: bool = False,
|
||||||
|
) -> Bootloader | None:
|
||||||
|
options = []
|
||||||
|
hidden_options = []
|
||||||
|
header = tr('Select bootloader to install')
|
||||||
|
|
||||||
|
default = Bootloader.get_default(uefi, skip_boot)
|
||||||
|
|
||||||
|
if not skip_boot:
|
||||||
|
hidden_options += [Bootloader.NO_BOOTLOADER]
|
||||||
|
|
||||||
|
if not uefi:
|
||||||
|
options += [Bootloader.Grub, Bootloader.Limine]
|
||||||
|
header += '\n' + tr('UEFI is not detected and some options are disabled')
|
||||||
|
else:
|
||||||
|
options += [b for b in Bootloader if b not in hidden_options]
|
||||||
|
|
||||||
|
items = [MenuItem(o.value, value=o) for o in options]
|
||||||
|
group = MenuItemGroup(items)
|
||||||
|
group.set_default_by_value(default)
|
||||||
|
group.set_focus_by_value(preset)
|
||||||
|
|
||||||
|
result = await Selection[Bootloader](
|
||||||
|
group,
|
||||||
|
header=header,
|
||||||
|
allow_skip=True,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case ResultType.Selection:
|
||||||
|
return result.get_value()
|
||||||
|
case ResultType.Reset:
|
||||||
|
raise ValueError('Unhandled result type')
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum, auto
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from archinstall.lib.hardware import SysInfo
|
||||||
|
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
|
||||||
|
from archinstall.lib.models.device import DiskLayoutConfiguration
|
||||||
|
|
||||||
|
|
||||||
|
class BootloaderValidationFailureKind(Enum):
|
||||||
|
LimineNonFatBoot = auto()
|
||||||
|
LimineLayout = auto()
|
||||||
|
BootloaderRequiresUefi = auto()
|
||||||
|
EfistubNonFatBoot = auto()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BootloaderValidationFailure:
|
||||||
|
kind: BootloaderValidationFailureKind
|
||||||
|
description: str
|
||||||
|
|
||||||
|
|
||||||
|
def validate_bootloader_layout(
|
||||||
|
bootloader_config: BootloaderConfiguration | None,
|
||||||
|
disk_config: DiskLayoutConfiguration | None,
|
||||||
|
) -> BootloaderValidationFailure | None:
|
||||||
|
"""Validate bootloader configuration against disk layout.
|
||||||
|
|
||||||
|
Returns a failure with a human-readable description if the configuration
|
||||||
|
would produce an unbootable system, or None if it is valid.
|
||||||
|
"""
|
||||||
|
if not (bootloader_config and disk_config):
|
||||||
|
return None
|
||||||
|
|
||||||
|
bootloader = bootloader_config.bootloader
|
||||||
|
|
||||||
|
if bootloader == Bootloader.NO_BOOTLOADER:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if bootloader.is_uefi_only() and not SysInfo.has_uefi():
|
||||||
|
return BootloaderValidationFailure(
|
||||||
|
kind=BootloaderValidationFailureKind.BootloaderRequiresUefi,
|
||||||
|
description=f'{bootloader.value} requires a UEFI system.',
|
||||||
|
)
|
||||||
|
|
||||||
|
boot_part = next(
|
||||||
|
(p for m in disk_config.device_modifications if (p := m.get_boot_partition())),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if bootloader == Bootloader.Efistub:
|
||||||
|
# The UEFI firmware reads the kernel directly from the boot partition,
|
||||||
|
# which must be FAT.
|
||||||
|
if boot_part and (boot_part.fs_type is None or not boot_part.fs_type.is_fat()):
|
||||||
|
return BootloaderValidationFailure(
|
||||||
|
kind=BootloaderValidationFailureKind.EfistubNonFatBoot,
|
||||||
|
description='Efistub does not support booting with a non-FAT boot partition.',
|
||||||
|
)
|
||||||
|
|
||||||
|
if bootloader == Bootloader.Limine:
|
||||||
|
# Limine reads its config and kernels from the boot partition, which
|
||||||
|
# must be FAT.
|
||||||
|
if boot_part and (boot_part.fs_type is None or not boot_part.fs_type.is_fat()):
|
||||||
|
return BootloaderValidationFailure(
|
||||||
|
kind=BootloaderValidationFailureKind.LimineNonFatBoot,
|
||||||
|
description='Limine does not support booting with a non-FAT boot partition.',
|
||||||
|
)
|
||||||
|
|
||||||
|
# When the ESP is the boot partition but mounted outside /boot and
|
||||||
|
# UKI is disabled, kernels end up on the root filesystem which
|
||||||
|
# Limine cannot access.
|
||||||
|
if not bootloader_config.uki:
|
||||||
|
efi_part = next(
|
||||||
|
(p for m in disk_config.device_modifications if (p := m.get_efi_partition())),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if efi_part and efi_part == boot_part and efi_part.mountpoint != Path('/boot'):
|
||||||
|
return BootloaderValidationFailure(
|
||||||
|
kind=BootloaderValidationFailureKind.LimineLayout,
|
||||||
|
description=(
|
||||||
|
f'Limine requires kernels on a FAT partition. The ESP is mounted at {efi_part.mountpoint}, '
|
||||||
|
'enable UKI or add a separate /boot partition to install Limine.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
@ -1,101 +1,18 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import secrets
|
|
||||||
import shlex
|
import shlex
|
||||||
import stat
|
import stat
|
||||||
import string
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from datetime import date, datetime
|
|
||||||
from enum import Enum
|
|
||||||
from pathlib import Path
|
|
||||||
from select import EPOLLHUP, EPOLLIN, epoll
|
from select import EPOLLHUP, EPOLLIN, epoll
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Any, override
|
from typing import Any, Self, override
|
||||||
|
|
||||||
from .exceptions import RequirementError, SysCallError
|
from archinstall.lib.exceptions import RequirementError, SysCallError
|
||||||
from .output import debug, error, logger
|
from archinstall.lib.log import debug, error, logger
|
||||||
|
from archinstall.lib.utils.encoding import clear_vt100_escape_codes
|
||||||
# https://stackoverflow.com/a/43627833/929999
|
|
||||||
_VT100_ESCAPE_REGEX = r'\x1B\[[?0-9;]*[a-zA-Z]'
|
|
||||||
_VT100_ESCAPE_REGEX_BYTES = _VT100_ESCAPE_REGEX.encode()
|
|
||||||
|
|
||||||
|
|
||||||
def generate_password(length: int = 64) -> str:
|
|
||||||
haystack = string.printable # digits, ascii_letters, punctuation (!"#$[] etc) and whitespace
|
|
||||||
return ''.join(secrets.choice(haystack) for _ in range(length))
|
|
||||||
|
|
||||||
|
|
||||||
def locate_binary(name: str) -> str:
|
|
||||||
if path := which(name):
|
|
||||||
return path
|
|
||||||
raise RequirementError(f'Binary {name} does not exist.')
|
|
||||||
|
|
||||||
|
|
||||||
def clear_vt100_escape_codes(data: bytes) -> bytes:
|
|
||||||
return re.sub(_VT100_ESCAPE_REGEX_BYTES, b'', data)
|
|
||||||
|
|
||||||
|
|
||||||
def clear_vt100_escape_codes_from_str(data: str) -> str:
|
|
||||||
return re.sub(_VT100_ESCAPE_REGEX, '', data)
|
|
||||||
|
|
||||||
|
|
||||||
def jsonify(obj: object, safe: bool = True) -> object:
|
|
||||||
"""
|
|
||||||
Converts objects into json.dumps() compatible nested dictionaries.
|
|
||||||
Setting safe to True skips dictionary keys starting with a bang (!)
|
|
||||||
"""
|
|
||||||
|
|
||||||
compatible_types = str, int, float, bool
|
|
||||||
if isinstance(obj, dict):
|
|
||||||
return {
|
|
||||||
key: jsonify(value, safe)
|
|
||||||
for key, value in obj.items()
|
|
||||||
if isinstance(key, compatible_types) and not (isinstance(key, str) and key.startswith('!') and safe)
|
|
||||||
}
|
|
||||||
if isinstance(obj, Enum):
|
|
||||||
return obj.value
|
|
||||||
if hasattr(obj, 'json'):
|
|
||||||
# json() is a friendly name for json-helper, it should return
|
|
||||||
# a dictionary representation of the object so that it can be
|
|
||||||
# processed by the json library.
|
|
||||||
return jsonify(obj.json(), safe)
|
|
||||||
if isinstance(obj, datetime | date):
|
|
||||||
return obj.isoformat()
|
|
||||||
if isinstance(obj, list | set | tuple):
|
|
||||||
return [jsonify(item, safe) for item in obj]
|
|
||||||
if isinstance(obj, Path):
|
|
||||||
return str(obj)
|
|
||||||
if hasattr(obj, '__dict__'):
|
|
||||||
return vars(obj)
|
|
||||||
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
class JSON(json.JSONEncoder, json.JSONDecoder):
|
|
||||||
"""
|
|
||||||
A safe JSON encoder that will omit private information in dicts (starting with !)
|
|
||||||
"""
|
|
||||||
|
|
||||||
@override
|
|
||||||
def encode(self, o: object) -> str:
|
|
||||||
return super().encode(jsonify(o))
|
|
||||||
|
|
||||||
|
|
||||||
class UNSAFE_JSON(json.JSONEncoder, json.JSONDecoder):
|
|
||||||
"""
|
|
||||||
UNSAFE_JSON will call/encode and keep private information in dicts (starting with !)
|
|
||||||
"""
|
|
||||||
|
|
||||||
@override
|
|
||||||
def encode(self, o: object) -> str:
|
|
||||||
return super().encode(jsonify(o, safe=False))
|
|
||||||
|
|
||||||
|
|
||||||
class SysCommandWorker:
|
class SysCommandWorker:
|
||||||
|
|
@ -127,8 +44,8 @@ class SysCommandWorker:
|
||||||
self._trace_log_pos = 0
|
self._trace_log_pos = 0
|
||||||
self.poll_object = epoll()
|
self.poll_object = epoll()
|
||||||
self.child_fd: int | None = None
|
self.child_fd: int | None = None
|
||||||
self.started: float | None = None
|
self.started = False
|
||||||
self.ended: float | None = None
|
self.ended = False
|
||||||
self.remove_vt100_escape_codes_from_lines: bool = remove_vt100_escape_codes_from_lines
|
self.remove_vt100_escape_codes_from_lines: bool = remove_vt100_escape_codes_from_lines
|
||||||
|
|
||||||
def __contains__(self, key: bytes) -> bool:
|
def __contains__(self, key: bytes) -> bool:
|
||||||
|
|
@ -168,7 +85,7 @@ class SysCommandWorker:
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
return str(self._trace_log)
|
return str(self._trace_log)
|
||||||
|
|
||||||
def __enter__(self) -> 'SysCommandWorker':
|
def __enter__(self) -> Self:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
|
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> None:
|
||||||
|
|
@ -200,7 +117,7 @@ class SysCommandWorker:
|
||||||
def is_alive(self) -> bool:
|
def is_alive(self) -> bool:
|
||||||
self.poll()
|
self.poll()
|
||||||
|
|
||||||
if self.started and self.ended is None:
|
if self.started and not self.ended:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
@ -256,11 +173,11 @@ class SysCommandWorker:
|
||||||
self.peak(output)
|
self.peak(output)
|
||||||
self._trace_log += output
|
self._trace_log += output
|
||||||
except OSError:
|
except OSError:
|
||||||
self.ended = time.time()
|
self.ended = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if self.ended or (not got_output and not _pid_exists(self.pid)):
|
if self.ended or (not got_output and not _pid_exists(self.pid)):
|
||||||
self.ended = time.time()
|
self.ended = True
|
||||||
try:
|
try:
|
||||||
wait_status = os.waitpid(self.pid, 0)[1]
|
wait_status = os.waitpid(self.pid, 0)[1]
|
||||||
self.exit_code = os.waitstatus_to_exitcode(wait_status)
|
self.exit_code = os.waitstatus_to_exitcode(wait_status)
|
||||||
|
|
@ -278,9 +195,9 @@ class SysCommandWorker:
|
||||||
os.chdir(str(self.working_directory))
|
os.chdir(str(self.working_directory))
|
||||||
|
|
||||||
# Note: If for any reason, we get a Python exception between here
|
# Note: If for any reason, we get a Python exception between here
|
||||||
# and until os.close(), the traceback will get locked inside
|
# and until os.close(), the traceback will get locked inside
|
||||||
# stdout of the child_fd object. `os.read(self.child_fd, 8192)` is the
|
# stdout of the child_fd object. `os.read(self.child_fd, 8192)` is the
|
||||||
# only way to get the traceback without losing it.
|
# only way to get the traceback without losing it.
|
||||||
|
|
||||||
self.pid, self.child_fd = pty.fork()
|
self.pid, self.child_fd = pty.fork()
|
||||||
|
|
||||||
|
|
@ -298,7 +215,7 @@ class SysCommandWorker:
|
||||||
# Only parent process moves back to the original working directory
|
# Only parent process moves back to the original working directory
|
||||||
os.chdir(old_dir)
|
os.chdir(old_dir)
|
||||||
|
|
||||||
self.started = time.time()
|
self.started = True
|
||||||
self.poll_object.register(self.child_fd, EPOLLIN | EPOLLHUP)
|
self.poll_object.register(self.child_fd, EPOLLIN | EPOLLHUP)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
@ -414,31 +331,6 @@ class SysCommand:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _append_log(file: str, content: str) -> None:
|
|
||||||
path = logger.directory / file
|
|
||||||
|
|
||||||
change_perm = not path.exists()
|
|
||||||
|
|
||||||
try:
|
|
||||||
with path.open('a') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
if change_perm:
|
|
||||||
path.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
|
|
||||||
except (PermissionError, FileNotFoundError):
|
|
||||||
# If the file does not exist, ignore the error
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _cmd_history(cmd: list[str]) -> None:
|
|
||||||
content = f'{time.time()} {cmd}\n'
|
|
||||||
_append_log('cmd_history.txt', content)
|
|
||||||
|
|
||||||
|
|
||||||
def _cmd_output(output: str) -> None:
|
|
||||||
_append_log('cmd_output.txt', output)
|
|
||||||
|
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
cmd: list[str],
|
cmd: list[str],
|
||||||
input_data: bytes | None = None,
|
input_data: bytes | None = None,
|
||||||
|
|
@ -454,8 +346,39 @@ def run(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
def _pid_exists(pid: int) -> bool:
|
||||||
try:
|
try:
|
||||||
return any(subprocess.check_output(['ps', '--no-headers', '-o', 'pid', '-p', str(pid)]).strip())
|
return any(subprocess.check_output(['ps', '--no-headers', '-o', 'pid', '-p', str(pid)]).strip())
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_history(cmd: list[str]) -> None:
|
||||||
|
content = f'{time.time()} {cmd}\n'
|
||||||
|
_append_log('cmd_history.txt', content)
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_output(output: str) -> None:
|
||||||
|
_append_log('cmd_output.txt', output)
|
||||||
|
|
||||||
|
|
||||||
|
def _append_log(file: str, content: str) -> None:
|
||||||
|
path = logger.directory / file
|
||||||
|
|
||||||
|
change_perm = not path.exists()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with path.open('a') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
if change_perm:
|
||||||
|
path.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
|
||||||
|
except PermissionError, FileNotFoundError:
|
||||||
|
# If the file does not exist, ignore the error
|
||||||
|
pass
|
||||||
|
|
@ -2,18 +2,20 @@ import json
|
||||||
import readline
|
import readline
|
||||||
import stat
|
import stat
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
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.translationhandler import tr
|
from archinstall.lib.translationhandler import tr
|
||||||
from archinstall.tui.curses_menu import SelectMenu, Tui
|
from archinstall.lib.utils.format import as_key_value_pair
|
||||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||||
from archinstall.tui.result import ResultType
|
from archinstall.tui.result import ResultType
|
||||||
from archinstall.tui.types import Alignment, FrameProperties, Orientation, PreviewStyle
|
|
||||||
|
|
||||||
from .args import ArchConfig
|
|
||||||
from .crypt import encrypt
|
|
||||||
from .general import JSON, UNSAFE_JSON
|
|
||||||
from .output import debug, logger, warn
|
|
||||||
from .utils.util import get_password, prompt_dir
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationOutput:
|
class ConfigurationOutput:
|
||||||
|
|
@ -41,43 +43,86 @@ class ConfigurationOutput:
|
||||||
return self._user_creds_file
|
return self._user_creds_file
|
||||||
|
|
||||||
def user_config_to_json(self) -> str:
|
def user_config_to_json(self) -> str:
|
||||||
out = self._config.safe_json()
|
config = self._config.safe_config()
|
||||||
return json.dumps(out, indent=4, sort_keys=True, cls=JSON)
|
|
||||||
|
adapter = TypeAdapter(dict[ArchConfigType, Any])
|
||||||
|
python_dict = adapter.dump_python(config)
|
||||||
|
return json.dumps(python_dict, indent=4, sort_keys=True)
|
||||||
|
|
||||||
def user_credentials_to_json(self) -> str:
|
def user_credentials_to_json(self) -> str:
|
||||||
out = self._config.unsafe_json()
|
cfg = self._config.unsafe_config()
|
||||||
return json.dumps(out, indent=4, sort_keys=True, cls=UNSAFE_JSON)
|
|
||||||
|
adapter = TypeAdapter(dict[ArchConfigType, Any])
|
||||||
|
python_dict = adapter.dump_python(cfg)
|
||||||
|
return json.dumps(python_dict, indent=4, sort_keys=True)
|
||||||
|
|
||||||
def write_debug(self) -> None:
|
def write_debug(self) -> None:
|
||||||
debug(' -- Chosen configuration --')
|
debug(' -- Chosen configuration --')
|
||||||
debug(self.user_config_to_json())
|
debug(self.user_config_to_json())
|
||||||
|
|
||||||
def confirm_config(self) -> bool:
|
def as_summary(self) -> str:
|
||||||
|
"""
|
||||||
|
Render a concise two-column summary of the current configuration.
|
||||||
|
|
||||||
|
Returns an empty string if nothing meaningful to show.
|
||||||
|
"""
|
||||||
|
cfg: dict[str, str | list[str] | bool] = {}
|
||||||
|
|
||||||
|
for key, value in self._config.plain_cfg().items():
|
||||||
|
cfg[key.text()] = value
|
||||||
|
|
||||||
|
for config_type, obj in self._config.sub_cfg().items():
|
||||||
|
if not hasattr(obj, 'summary'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
summary = obj.summary()
|
||||||
|
if summary:
|
||||||
|
cfg[config_type.text()] = summary
|
||||||
|
|
||||||
|
simple_summary = as_key_value_pair(cfg, ignore_empty=True)
|
||||||
|
|
||||||
|
return simple_summary
|
||||||
|
|
||||||
|
async def confirm_config(self, show_install_warnings: bool = False) -> bool:
|
||||||
header = f'{tr("The specified configuration will be applied")}. '
|
header = f'{tr("The specified configuration will be applied")}. '
|
||||||
header += tr('Would you like to continue?') + '\n'
|
header += tr('Would you like to continue?') + '\n'
|
||||||
|
|
||||||
with Tui():
|
if show_install_warnings:
|
||||||
group = MenuItemGroup.yes_no()
|
header += self._render_install_warnings()
|
||||||
group.focus_item = MenuItem.yes()
|
|
||||||
group.set_preview_for_all(lambda x: self.user_config_to_json())
|
|
||||||
|
|
||||||
result = SelectMenu[bool](
|
group = MenuItemGroup.yes_no()
|
||||||
group,
|
group.set_preview_for_all(lambda x: self.user_config_to_json())
|
||||||
header=header,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
columns=2,
|
|
||||||
orientation=Orientation.HORIZONTAL,
|
|
||||||
allow_skip=False,
|
|
||||||
preview_size='auto',
|
|
||||||
preview_style=PreviewStyle.BOTTOM,
|
|
||||||
preview_frame=FrameProperties.max(tr('Configuration')),
|
|
||||||
).run()
|
|
||||||
|
|
||||||
if result.item() != MenuItem.yes():
|
result = await Confirmation(
|
||||||
return False
|
group=group,
|
||||||
|
header=header,
|
||||||
|
allow_skip=False,
|
||||||
|
preset=True,
|
||||||
|
preview_location='bottom',
|
||||||
|
preview_header=tr('Configuration preview'),
|
||||||
|
).show()
|
||||||
|
|
||||||
|
if not result.get_value():
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def get_install_warnings(self) -> list[str]:
|
||||||
|
warnings: list[str] = []
|
||||||
|
|
||||||
|
if not isinstance(self._config.network_config, NetworkConfiguration):
|
||||||
|
warnings.append(tr('Warning: no network configuration selected. Network will need to be set up manually on the installed system.'))
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
def _render_install_warnings(self) -> str:
|
||||||
|
warnings = self.get_install_warnings()
|
||||||
|
|
||||||
|
if not warnings:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
return '\n' + '\n'.join(f'[yellow]{w}[/]' for w in warnings) + '\n'
|
||||||
|
|
||||||
def _is_valid_path(self, dest_path: Path) -> bool:
|
def _is_valid_path(self, dest_path: Path) -> bool:
|
||||||
dest_path_ok = dest_path.exists() and dest_path.is_dir()
|
dest_path_ok = dest_path.exists() and dest_path.is_dir()
|
||||||
if not dest_path_ok:
|
if not dest_path_ok:
|
||||||
|
|
@ -122,7 +167,7 @@ class ConfigurationOutput:
|
||||||
self.save_user_creds(save_path, password=password)
|
self.save_user_creds(save_path, password=password)
|
||||||
|
|
||||||
|
|
||||||
def save_config(config: ArchConfig) -> None:
|
async def save_config(config: ArchConfig) -> None:
|
||||||
def preview(item: MenuItem) -> str | None:
|
def preview(item: MenuItem) -> str | None:
|
||||||
match item.value:
|
match item.value:
|
||||||
case 'user_config':
|
case 'user_config':
|
||||||
|
|
@ -160,13 +205,11 @@ def save_config(config: ArchConfig) -> None:
|
||||||
]
|
]
|
||||||
|
|
||||||
group = MenuItemGroup(items)
|
group = MenuItemGroup(items)
|
||||||
result = SelectMenu[str](
|
result = await Selection[str](
|
||||||
group,
|
group,
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
preview_frame=FrameProperties.max(tr('Configuration')),
|
preview_location='right',
|
||||||
preview_size='auto',
|
).show()
|
||||||
preview_style=PreviewStyle.RIGHT,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
match result.type_:
|
||||||
case ResultType.Skip:
|
case ResultType.Skip:
|
||||||
|
|
@ -179,9 +222,8 @@ def save_config(config: ArchConfig) -> None:
|
||||||
readline.set_completer_delims('\t\n=')
|
readline.set_completer_delims('\t\n=')
|
||||||
readline.parse_and_bind('tab: complete')
|
readline.parse_and_bind('tab: complete')
|
||||||
|
|
||||||
dest_path = prompt_dir(
|
dest_path = await prompt_dir(
|
||||||
tr('Directory'),
|
tr('Enter a directory for the configuration(s) to be saved') + '\n',
|
||||||
tr('Enter a directory for the configuration(s) to be saved (tab completion enabled)') + '\n',
|
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -190,50 +232,39 @@ def save_config(config: ArchConfig) -> None:
|
||||||
|
|
||||||
header = tr('Do you want to save the configuration file(s) to {}?').format(dest_path)
|
header = tr('Do you want to save the configuration file(s) to {}?').format(dest_path)
|
||||||
|
|
||||||
group = MenuItemGroup.yes_no()
|
save_result = await Confirmation(
|
||||||
group.focus_item = MenuItem.yes()
|
|
||||||
|
|
||||||
result = SelectMenu(
|
|
||||||
group,
|
|
||||||
header=header,
|
header=header,
|
||||||
allow_skip=False,
|
allow_skip=False,
|
||||||
alignment=Alignment.CENTER,
|
preset=True,
|
||||||
columns=2,
|
).show()
|
||||||
orientation=Orientation.HORIZONTAL,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
match save_result.type_:
|
||||||
case ResultType.Selection:
|
case ResultType.Selection:
|
||||||
if result.item() == MenuItem.no():
|
if not save_result.get_value():
|
||||||
return
|
return
|
||||||
|
case _:
|
||||||
|
return
|
||||||
|
|
||||||
debug(f'Saving configuration files to {dest_path.absolute()}')
|
debug(f'Saving configuration files to {dest_path.absolute()}')
|
||||||
|
|
||||||
header = tr('Do you want to encrypt the user_credentials.json file?')
|
header = tr('Do you want to encrypt the user_credentials.json file?')
|
||||||
|
|
||||||
group = MenuItemGroup.yes_no()
|
enc_result = await Confirmation(
|
||||||
group.focus_item = MenuItem.no()
|
|
||||||
|
|
||||||
result = SelectMenu(
|
|
||||||
group,
|
|
||||||
header=header,
|
header=header,
|
||||||
allow_skip=False,
|
allow_skip=False,
|
||||||
alignment=Alignment.CENTER,
|
preset=False,
|
||||||
columns=2,
|
).show()
|
||||||
orientation=Orientation.HORIZONTAL,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
enc_password: str | None = None
|
enc_password: str | None = None
|
||||||
match result.type_:
|
if enc_result.type_ == ResultType.Selection:
|
||||||
case ResultType.Selection:
|
if enc_result.get_value():
|
||||||
if result.item() == MenuItem.yes():
|
password = await get_password(
|
||||||
password = get_password(
|
header=tr('Credentials file encryption password'),
|
||||||
text=tr('Credentials file encryption password'),
|
allow_skip=True,
|
||||||
allow_skip=True,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if password:
|
if password:
|
||||||
enc_password = password.plaintext
|
enc_password = password.plaintext
|
||||||
|
|
||||||
match save_option:
|
match save_option:
|
||||||
case 'user_config':
|
case 'user_config':
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from pathlib import Path
|
||||||
from cryptography.fernet import Fernet, InvalidToken
|
from cryptography.fernet import Fernet, InvalidToken
|
||||||
from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
|
from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
|
||||||
|
|
||||||
from .output import debug
|
from archinstall.lib.log import debug
|
||||||
|
|
||||||
libcrypt = ctypes.CDLL('libcrypt.so')
|
libcrypt = ctypes.CDLL('libcrypt.so')
|
||||||
|
|
||||||
|
|
@ -48,10 +48,10 @@ def crypt_gen_salt(prefix: str | bytes, rounds: int) -> bytes:
|
||||||
|
|
||||||
def crypt_yescrypt(plaintext: str) -> str:
|
def crypt_yescrypt(plaintext: str) -> str:
|
||||||
"""
|
"""
|
||||||
By default chpasswd in Arch uses PAM to to hash the password with crypt_yescrypt
|
By default chpasswd in Arch uses PAM to hash the password with crypt_yescrypt
|
||||||
the PAM code https://github.com/linux-pam/linux-pam/blob/master/modules/pam_unix/support.c
|
the PAM code https://github.com/linux-pam/linux-pam/blob/master/modules/pam_unix/support.c
|
||||||
shows that the hashing rounds are determined from YESCRYPT_COST_FACTOR in /etc/login.defs
|
shows that the hashing rounds are determined from YESCRYPT_COST_FACTOR in /etc/login.defs
|
||||||
If no value was specified (or commented out) a default of 5 is choosen
|
If no value was specified (or commented out) a default of 5 is chosen
|
||||||
"""
|
"""
|
||||||
value = _search_login_defs('YESCRYPT_COST_FACTOR')
|
value = _search_login_defs('YESCRYPT_COST_FACTOR')
|
||||||
if value is not None:
|
if value is not None:
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,22 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
from collections.abc import Iterable
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Literal, overload
|
|
||||||
|
|
||||||
from parted import Device, Disk, DiskException, FileSystem, Geometry, IOException, Partition, PartitionException, freshDisk, getAllDevices, getDevice, newDisk
|
from parted import Device, Disk, DiskException, FileSystem, Geometry, IOException, Partition, PartitionException, freshDisk, getAllDevices, getDevice, newDisk
|
||||||
|
|
||||||
from ..exceptions import DiskError, SysCallError, UnknownFilesystemFormat
|
from archinstall.lib.command import SysCommand
|
||||||
from ..general import SysCommand, SysCommandWorker
|
from archinstall.lib.disk.luks import Luks2, unlock_luks2_dev
|
||||||
from ..luks import Luks2
|
from archinstall.lib.disk.utils import (
|
||||||
from ..models.device import (
|
find_lsblk_info,
|
||||||
|
get_all_lsblk_info,
|
||||||
|
get_lsblk_info,
|
||||||
|
mount,
|
||||||
|
udev_sync,
|
||||||
|
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,
|
DEFAULT_ITER_TIME,
|
||||||
BDevice,
|
BDevice,
|
||||||
BtrfsMountOption,
|
BtrfsMountOption,
|
||||||
|
|
@ -21,33 +24,19 @@ from ..models.device import (
|
||||||
DiskEncryption,
|
DiskEncryption,
|
||||||
FilesystemType,
|
FilesystemType,
|
||||||
LsblkInfo,
|
LsblkInfo,
|
||||||
LvmGroupInfo,
|
|
||||||
LvmPVInfo,
|
|
||||||
LvmVolume,
|
|
||||||
LvmVolumeGroup,
|
|
||||||
LvmVolumeInfo,
|
|
||||||
ModificationStatus,
|
ModificationStatus,
|
||||||
PartitionFlag,
|
PartitionFlag,
|
||||||
PartitionGUID,
|
PartitionGUID,
|
||||||
PartitionModification,
|
PartitionModification,
|
||||||
PartitionTable,
|
PartitionTable,
|
||||||
SectorSize,
|
|
||||||
Size,
|
|
||||||
SubvolumeModification,
|
SubvolumeModification,
|
||||||
Unit,
|
Unit,
|
||||||
_BtrfsSubvolumeInfo,
|
_BtrfsSubvolumeInfo,
|
||||||
_DeviceInfo,
|
_DeviceInfo,
|
||||||
_PartitionInfo,
|
_PartitionInfo,
|
||||||
)
|
)
|
||||||
from ..models.users import Password
|
from archinstall.lib.models.users import Password
|
||||||
from ..output import debug, error, info, log
|
from archinstall.lib.pathnames import ARCHISO_MOUNTPOINT
|
||||||
from ..utils.util import is_subpath
|
|
||||||
from .utils import (
|
|
||||||
find_lsblk_info,
|
|
||||||
get_all_lsblk_info,
|
|
||||||
get_lsblk_info,
|
|
||||||
umount,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceHandler:
|
class DeviceHandler:
|
||||||
|
|
@ -69,13 +58,11 @@ class DeviceHandler:
|
||||||
def load_devices(self) -> None:
|
def load_devices(self) -> None:
|
||||||
block_devices = {}
|
block_devices = {}
|
||||||
|
|
||||||
self.udev_sync()
|
udev_sync()
|
||||||
all_lsblk_info = get_all_lsblk_info()
|
all_lsblk_info = get_all_lsblk_info()
|
||||||
devices = getAllDevices()
|
devices = getAllDevices()
|
||||||
devices.extend(self.get_loop_devices())
|
devices.extend(self.get_loop_devices())
|
||||||
|
|
||||||
archiso_mountpoint = Path('/run/archiso/airootfs')
|
|
||||||
|
|
||||||
for device in devices:
|
for device in devices:
|
||||||
dev_lsblk_info = find_lsblk_info(device.path, all_lsblk_info)
|
dev_lsblk_info = find_lsblk_info(device.path, all_lsblk_info)
|
||||||
|
|
||||||
|
|
@ -87,7 +74,7 @@ class DeviceHandler:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# exclude archiso loop device
|
# exclude archiso loop device
|
||||||
if dev_lsblk_info.mountpoint == archiso_mountpoint:
|
if dev_lsblk_info.mountpoint == ARCHISO_MOUNTPOINT:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -112,7 +99,7 @@ class DeviceHandler:
|
||||||
fs_type = self._determine_fs_type(partition, lsblk_info)
|
fs_type = self._determine_fs_type(partition, lsblk_info)
|
||||||
subvol_infos = []
|
subvol_infos = []
|
||||||
|
|
||||||
if fs_type == FilesystemType.Btrfs:
|
if fs_type == FilesystemType.BTRFS:
|
||||||
subvol_infos = self.get_btrfs_info(partition.path, lsblk_info)
|
subvol_infos = self.get_btrfs_info(partition.path, lsblk_info)
|
||||||
|
|
||||||
partition_infos.append(
|
partition_infos.append(
|
||||||
|
|
@ -160,8 +147,8 @@ class DeviceHandler:
|
||||||
) -> FilesystemType | None:
|
) -> FilesystemType | None:
|
||||||
try:
|
try:
|
||||||
if partition.fileSystem:
|
if partition.fileSystem:
|
||||||
if partition.fileSystem.type == FilesystemType.LinuxSwap.parted_value:
|
if partition.fileSystem.type == FilesystemType.LINUX_SWAP.parted_value:
|
||||||
return FilesystemType.LinuxSwap
|
return FilesystemType.LINUX_SWAP
|
||||||
return FilesystemType(partition.fileSystem.type)
|
return FilesystemType(partition.fileSystem.type)
|
||||||
elif lsblk_info is not None:
|
elif lsblk_info is not None:
|
||||||
return FilesystemType(lsblk_info.fstype) if lsblk_info.fstype else None
|
return FilesystemType(lsblk_info.fstype) if lsblk_info.fstype else None
|
||||||
|
|
@ -188,23 +175,6 @@ class DeviceHandler:
|
||||||
return part
|
return part
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_parent_device_path(self, dev_path: Path) -> Path:
|
|
||||||
lsblk = get_lsblk_info(dev_path)
|
|
||||||
return Path(f'/dev/{lsblk.pkname}')
|
|
||||||
|
|
||||||
def get_unique_path_for_device(self, dev_path: Path) -> Path | None:
|
|
||||||
paths = Path('/dev/disk/by-id').glob('*')
|
|
||||||
linked_targets = {p.resolve(): p for p in paths}
|
|
||||||
linked_wwn_targets = {p: linked_targets[p] for p in linked_targets if p.name.startswith('wwn-') or p.name.startswith('nvme-eui.')}
|
|
||||||
|
|
||||||
if dev_path in linked_wwn_targets:
|
|
||||||
return linked_wwn_targets[dev_path]
|
|
||||||
|
|
||||||
if dev_path in linked_targets:
|
|
||||||
return linked_targets[dev_path]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_uuid_for_path(self, path: Path) -> str | None:
|
def get_uuid_for_path(self, path: Path) -> str | None:
|
||||||
partition = self.find_partition(path)
|
partition = self.find_partition(path)
|
||||||
return partition.partuuid if partition else None
|
return partition.partuuid if partition else None
|
||||||
|
|
@ -220,7 +190,7 @@ class DeviceHandler:
|
||||||
subvol_infos: list[_BtrfsSubvolumeInfo] = []
|
subvol_infos: list[_BtrfsSubvolumeInfo] = []
|
||||||
|
|
||||||
if not lsblk_info.mountpoint:
|
if not lsblk_info.mountpoint:
|
||||||
self.mount(dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
|
mount(dev_path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
|
||||||
mountpoint = self._TMP_BTRFS_MOUNT
|
mountpoint = self._TMP_BTRFS_MOUNT
|
||||||
else:
|
else:
|
||||||
# when multiple subvolumes are mounted then the lsblk output may look like
|
# when multiple subvolumes are mounted then the lsblk output may look like
|
||||||
|
|
@ -271,20 +241,20 @@ class DeviceHandler:
|
||||||
options = []
|
options = []
|
||||||
|
|
||||||
match fs_type:
|
match fs_type:
|
||||||
case FilesystemType.Btrfs | FilesystemType.F2fs | FilesystemType.Xfs:
|
case FilesystemType.BTRFS | FilesystemType.XFS:
|
||||||
# Force overwrite
|
# Force overwrite
|
||||||
options.append('-f')
|
options.append('-f')
|
||||||
case FilesystemType.Ext2 | FilesystemType.Ext3 | FilesystemType.Ext4:
|
case FilesystemType.F2FS:
|
||||||
|
options.append('-f')
|
||||||
|
options.extend(('-O', 'extra_attr'))
|
||||||
|
case FilesystemType.EXT2 | FilesystemType.EXT3 | FilesystemType.EXT4:
|
||||||
# Force create
|
# Force create
|
||||||
options.append('-F')
|
options.append('-F')
|
||||||
case FilesystemType.Fat12 | FilesystemType.Fat16 | FilesystemType.Fat32:
|
case _ if fs_type.is_fat():
|
||||||
mkfs_type = 'fat'
|
mkfs_type = 'fat'
|
||||||
# Set FAT size
|
# Set FAT size
|
||||||
options.extend(('-F', fs_type.value.removeprefix(mkfs_type)))
|
options.extend(('-F', fs_type.value.removeprefix(mkfs_type)))
|
||||||
case FilesystemType.Ntfs:
|
case FilesystemType.LINUX_SWAP:
|
||||||
# Skip zeroing and bad sector check
|
|
||||||
options.append('--fast')
|
|
||||||
case FilesystemType.LinuxSwap:
|
|
||||||
command = 'mkswap'
|
command = 'mkswap'
|
||||||
case _:
|
case _:
|
||||||
raise UnknownFilesystemFormat(f'Filetype "{fs_type.value}" is not supported')
|
raise UnknownFilesystemFormat(f'Filetype "{fs_type.value}" is not supported')
|
||||||
|
|
@ -319,7 +289,7 @@ class DeviceHandler:
|
||||||
|
|
||||||
key_file = luks_handler.encrypt(iter_time=iter_time)
|
key_file = luks_handler.encrypt(iter_time=iter_time)
|
||||||
|
|
||||||
self.udev_sync()
|
udev_sync()
|
||||||
|
|
||||||
luks_handler.unlock(key_file=key_file)
|
luks_handler.unlock(key_file=key_file)
|
||||||
|
|
||||||
|
|
@ -350,7 +320,7 @@ class DeviceHandler:
|
||||||
|
|
||||||
key_file = luks_handler.encrypt(iter_time=enc_conf.iter_time)
|
key_file = luks_handler.encrypt(iter_time=enc_conf.iter_time)
|
||||||
|
|
||||||
self.udev_sync()
|
udev_sync()
|
||||||
|
|
||||||
luks_handler.unlock(key_file=key_file)
|
luks_handler.unlock(key_file=key_file)
|
||||||
|
|
||||||
|
|
@ -363,145 +333,6 @@ class DeviceHandler:
|
||||||
info(f'luks2 locking device: {dev_path}')
|
info(f'luks2 locking device: {dev_path}')
|
||||||
luks_handler.lock()
|
luks_handler.lock()
|
||||||
|
|
||||||
def _lvm_info(
|
|
||||||
self,
|
|
||||||
cmd: str,
|
|
||||||
info_type: Literal['lv', 'vg', 'pvseg'],
|
|
||||||
) -> LvmVolumeInfo | LvmGroupInfo | LvmPVInfo | None:
|
|
||||||
raw_info = SysCommand(cmd).decode().split('\n')
|
|
||||||
|
|
||||||
# for whatever reason the output sometimes contains
|
|
||||||
# "File descriptor X leaked leaked on vgs invocation
|
|
||||||
data = '\n'.join([raw for raw in raw_info if 'File descriptor' not in raw])
|
|
||||||
|
|
||||||
debug(f'LVM info: {data}')
|
|
||||||
|
|
||||||
reports = json.loads(data)
|
|
||||||
|
|
||||||
for report in reports['report']:
|
|
||||||
if len(report[info_type]) != 1:
|
|
||||||
raise ValueError('Report does not contain any entry')
|
|
||||||
|
|
||||||
entry = report[info_type][0]
|
|
||||||
|
|
||||||
match info_type:
|
|
||||||
case 'pvseg':
|
|
||||||
return LvmPVInfo(
|
|
||||||
pv_name=Path(entry['pv_name']),
|
|
||||||
lv_name=entry['lv_name'],
|
|
||||||
vg_name=entry['vg_name'],
|
|
||||||
)
|
|
||||||
case 'lv':
|
|
||||||
return LvmVolumeInfo(
|
|
||||||
lv_name=entry['lv_name'],
|
|
||||||
vg_name=entry['vg_name'],
|
|
||||||
lv_size=Size(int(entry['lv_size'][:-1]), Unit.B, SectorSize.default()),
|
|
||||||
)
|
|
||||||
case 'vg':
|
|
||||||
return LvmGroupInfo(
|
|
||||||
vg_uuid=entry['vg_uuid'],
|
|
||||||
vg_size=Size(int(entry['vg_size'][:-1]), Unit.B, SectorSize.default()),
|
|
||||||
)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def _lvm_info_with_retry(self, cmd: str, info_type: Literal['lv']) -> LvmVolumeInfo | None: ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def _lvm_info_with_retry(self, cmd: str, info_type: Literal['vg']) -> LvmGroupInfo | None: ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def _lvm_info_with_retry(self, cmd: str, info_type: Literal['pvseg']) -> LvmPVInfo | None: ...
|
|
||||||
|
|
||||||
def _lvm_info_with_retry(
|
|
||||||
self,
|
|
||||||
cmd: str,
|
|
||||||
info_type: Literal['lv', 'vg', 'pvseg'],
|
|
||||||
) -> LvmVolumeInfo | LvmGroupInfo | LvmPVInfo | None:
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
return self._lvm_info(cmd, info_type)
|
|
||||||
except ValueError:
|
|
||||||
time.sleep(3)
|
|
||||||
|
|
||||||
def lvm_vol_info(self, lv_name: str) -> LvmVolumeInfo | None:
|
|
||||||
cmd = f'lvs --reportformat json --unit B -S lv_name={lv_name}'
|
|
||||||
|
|
||||||
return self._lvm_info_with_retry(cmd, 'lv')
|
|
||||||
|
|
||||||
def lvm_group_info(self, vg_name: str) -> LvmGroupInfo | None:
|
|
||||||
cmd = f'vgs --reportformat json --unit B -o vg_name,vg_uuid,vg_size -S vg_name={vg_name}'
|
|
||||||
|
|
||||||
return self._lvm_info_with_retry(cmd, 'vg')
|
|
||||||
|
|
||||||
def lvm_pvseg_info(self, vg_name: str, lv_name: str) -> LvmPVInfo | None:
|
|
||||||
cmd = f'pvs --segments -o+lv_name,vg_name -S vg_name={vg_name},lv_name={lv_name} --reportformat json '
|
|
||||||
|
|
||||||
return self._lvm_info_with_retry(cmd, 'pvseg')
|
|
||||||
|
|
||||||
def lvm_vol_change(self, vol: LvmVolume, activate: bool) -> None:
|
|
||||||
active_flag = 'y' if activate else 'n'
|
|
||||||
cmd = f'lvchange -a {active_flag} {vol.safe_dev_path}'
|
|
||||||
|
|
||||||
debug(f'lvchange volume: {cmd}')
|
|
||||||
SysCommand(cmd)
|
|
||||||
|
|
||||||
def lvm_export_vg(self, vg: LvmVolumeGroup) -> None:
|
|
||||||
cmd = f'vgexport {vg.name}'
|
|
||||||
|
|
||||||
debug(f'vgexport: {cmd}')
|
|
||||||
SysCommand(cmd)
|
|
||||||
|
|
||||||
def lvm_import_vg(self, vg: LvmVolumeGroup) -> None:
|
|
||||||
cmd = f'vgimport {vg.name}'
|
|
||||||
|
|
||||||
debug(f'vgimport: {cmd}')
|
|
||||||
SysCommand(cmd)
|
|
||||||
|
|
||||||
def lvm_vol_reduce(self, vol_path: Path, amount: Size) -> None:
|
|
||||||
val = amount.format_size(Unit.B, include_unit=False)
|
|
||||||
cmd = f'lvreduce -L -{val}B {vol_path}'
|
|
||||||
|
|
||||||
debug(f'Reducing LVM volume size: {cmd}')
|
|
||||||
SysCommand(cmd)
|
|
||||||
|
|
||||||
def lvm_pv_create(self, pvs: Iterable[Path]) -> None:
|
|
||||||
cmd = 'pvcreate ' + ' '.join([str(pv) for pv in pvs])
|
|
||||||
debug(f'Creating LVM PVS: {cmd}')
|
|
||||||
|
|
||||||
worker = SysCommandWorker(cmd)
|
|
||||||
worker.poll()
|
|
||||||
worker.write(b'y\n', line_ending=False)
|
|
||||||
|
|
||||||
def lvm_vg_create(self, pvs: Iterable[Path], vg_name: str) -> None:
|
|
||||||
pvs_str = ' '.join([str(pv) for pv in pvs])
|
|
||||||
cmd = f'vgcreate --yes {vg_name} {pvs_str}'
|
|
||||||
|
|
||||||
debug(f'Creating LVM group: {cmd}')
|
|
||||||
|
|
||||||
worker = SysCommandWorker(cmd)
|
|
||||||
worker.poll()
|
|
||||||
worker.write(b'y\n', line_ending=False)
|
|
||||||
|
|
||||||
def lvm_vol_create(self, vg_name: str, volume: LvmVolume, offset: Size | None = None) -> None:
|
|
||||||
if offset is not None:
|
|
||||||
length = volume.length - offset
|
|
||||||
else:
|
|
||||||
length = volume.length
|
|
||||||
|
|
||||||
length_str = length.format_size(Unit.B, include_unit=False)
|
|
||||||
cmd = f'lvcreate --yes -L {length_str}B {vg_name} -n {volume.name}'
|
|
||||||
|
|
||||||
debug(f'Creating volume: {cmd}')
|
|
||||||
|
|
||||||
worker = SysCommandWorker(cmd)
|
|
||||||
worker.poll()
|
|
||||||
worker.write(b'y\n', line_ending=False)
|
|
||||||
|
|
||||||
volume.vg_name = vg_name
|
|
||||||
volume.dev_path = Path(f'/dev/{vg_name}/{volume.name}')
|
|
||||||
|
|
||||||
def _setup_partition(
|
def _setup_partition(
|
||||||
self,
|
self,
|
||||||
part_mod: PartitionModification,
|
part_mod: PartitionModification,
|
||||||
|
|
@ -511,7 +342,7 @@ class DeviceHandler:
|
||||||
) -> None:
|
) -> None:
|
||||||
# when we require a delete and the partition to be (re)created
|
# when we require a delete and the partition to be (re)created
|
||||||
# already exists then we have to delete it first
|
# already exists then we have to delete it first
|
||||||
if requires_delete and part_mod.status in [ModificationStatus.Modify, ModificationStatus.Delete]:
|
if requires_delete and part_mod.status in [ModificationStatus.MODIFY, ModificationStatus.DELETE]:
|
||||||
info(f'Delete existing partition: {part_mod.safe_dev_path}')
|
info(f'Delete existing partition: {part_mod.safe_dev_path}')
|
||||||
part_info = self.find_partition(part_mod.safe_dev_path)
|
part_info = self.find_partition(part_mod.safe_dev_path)
|
||||||
|
|
||||||
|
|
@ -520,7 +351,7 @@ class DeviceHandler:
|
||||||
|
|
||||||
disk.deletePartition(part_info.partition)
|
disk.deletePartition(part_info.partition)
|
||||||
|
|
||||||
if part_mod.status == ModificationStatus.Delete:
|
if part_mod.status == ModificationStatus.DELETE:
|
||||||
return
|
return
|
||||||
|
|
||||||
start_sector = part_mod.start.convert(
|
start_sector = part_mod.start.convert(
|
||||||
|
|
@ -597,7 +428,7 @@ class DeviceHandler:
|
||||||
) -> None:
|
) -> None:
|
||||||
info(f'Creating subvolumes: {path}')
|
info(f'Creating subvolumes: {path}')
|
||||||
|
|
||||||
self.mount(path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
|
mount(path, self._TMP_BTRFS_MOUNT, create_target_mountpoint=True)
|
||||||
|
|
||||||
for sub_vol in sorted(btrfs_subvols, key=lambda x: x.name):
|
for sub_vol in sorted(btrfs_subvols, key=lambda x: x.name):
|
||||||
debug(f'Creating subvolume: {sub_vol.name}')
|
debug(f'Creating subvolume: {sub_vol.name}')
|
||||||
|
|
@ -632,7 +463,7 @@ class DeviceHandler:
|
||||||
if not part_mod.mapper_name:
|
if not part_mod.mapper_name:
|
||||||
raise ValueError('No device path specified for modification')
|
raise ValueError('No device path specified for modification')
|
||||||
|
|
||||||
luks_handler = self.unlock_luks2_dev(
|
luks_handler = unlock_luks2_dev(
|
||||||
part_mod.safe_dev_path,
|
part_mod.safe_dev_path,
|
||||||
part_mod.mapper_name,
|
part_mod.mapper_name,
|
||||||
enc_conf.encryption_password,
|
enc_conf.encryption_password,
|
||||||
|
|
@ -646,7 +477,7 @@ class DeviceHandler:
|
||||||
luks_handler = None
|
luks_handler = None
|
||||||
dev_path = part_mod.safe_dev_path
|
dev_path = part_mod.safe_dev_path
|
||||||
|
|
||||||
self.mount(
|
mount(
|
||||||
dev_path,
|
dev_path,
|
||||||
self._TMP_BTRFS_MOUNT,
|
self._TMP_BTRFS_MOUNT,
|
||||||
create_target_mountpoint=True,
|
create_target_mountpoint=True,
|
||||||
|
|
@ -665,19 +496,6 @@ class DeviceHandler:
|
||||||
if luks_handler is not None and luks_handler.mapper_dev is not None:
|
if luks_handler is not None and luks_handler.mapper_dev is not None:
|
||||||
luks_handler.lock()
|
luks_handler.lock()
|
||||||
|
|
||||||
def unlock_luks2_dev(
|
|
||||||
self,
|
|
||||||
dev_path: Path,
|
|
||||||
mapper_name: str,
|
|
||||||
enc_password: Password | None,
|
|
||||||
) -> Luks2:
|
|
||||||
luks_handler = Luks2(dev_path, mapper_name=mapper_name, password=enc_password)
|
|
||||||
|
|
||||||
if not luks_handler.is_unlocked():
|
|
||||||
luks_handler.unlock()
|
|
||||||
|
|
||||||
return luks_handler
|
|
||||||
|
|
||||||
def umount_all_existing(self, device_path: Path) -> None:
|
def umount_all_existing(self, device_path: Path) -> None:
|
||||||
debug(f'Unmounting all existing partitions: {device_path}')
|
debug(f'Unmounting all existing partitions: {device_path}')
|
||||||
|
|
||||||
|
|
@ -687,7 +505,7 @@ class DeviceHandler:
|
||||||
debug(f'Unmounting: {partition.path}')
|
debug(f'Unmounting: {partition.path}')
|
||||||
|
|
||||||
# un-mount for existing encrypted partitions
|
# un-mount for existing encrypted partitions
|
||||||
if partition.fs_type == FilesystemType.Crypto_luks:
|
if partition.fs_type == FilesystemType.CRYPTO_LUKS:
|
||||||
Luks2(partition.path).lock()
|
Luks2(partition.path).lock()
|
||||||
else:
|
else:
|
||||||
umount(partition.path, recursive=True)
|
umount(partition.path, recursive=True)
|
||||||
|
|
@ -726,49 +544,16 @@ class DeviceHandler:
|
||||||
|
|
||||||
disk.commit()
|
disk.commit()
|
||||||
|
|
||||||
@staticmethod
|
# Wipe filesystem/LVM signatures from newly created partitions
|
||||||
def swapon(path: Path) -> None:
|
# to prevent "signature detected" errors
|
||||||
try:
|
for part_mod in filtered_part:
|
||||||
SysCommand(['swapon', str(path)])
|
if part_mod.dev_path:
|
||||||
except SysCallError as err:
|
debug(f'Wiping signatures from: {part_mod.dev_path}')
|
||||||
raise DiskError(f'Could not enable swap {path}:\n{err.message}')
|
SysCommand(f'wipefs --all {part_mod.dev_path}')
|
||||||
|
|
||||||
def mount(
|
# Sync with udev after wiping signatures
|
||||||
self,
|
if filtered_part:
|
||||||
dev_path: Path,
|
udev_sync()
|
||||||
target_mountpoint: Path,
|
|
||||||
mount_fs: str | None = None,
|
|
||||||
create_target_mountpoint: bool = True,
|
|
||||||
options: list[str] = [],
|
|
||||||
) -> None:
|
|
||||||
if create_target_mountpoint and not target_mountpoint.exists():
|
|
||||||
target_mountpoint.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
if not target_mountpoint.exists():
|
|
||||||
raise ValueError('Target mountpoint does not exist')
|
|
||||||
|
|
||||||
lsblk_info = get_lsblk_info(dev_path)
|
|
||||||
if target_mountpoint in lsblk_info.mountpoints:
|
|
||||||
info(f'Device already mounted at {target_mountpoint}')
|
|
||||||
return
|
|
||||||
|
|
||||||
cmd = ['mount']
|
|
||||||
|
|
||||||
if len(options):
|
|
||||||
cmd.extend(('-o', ','.join(options)))
|
|
||||||
if mount_fs:
|
|
||||||
cmd.extend(('-t', mount_fs))
|
|
||||||
|
|
||||||
cmd.extend((str(dev_path), str(target_mountpoint)))
|
|
||||||
|
|
||||||
command = ' '.join(cmd)
|
|
||||||
|
|
||||||
debug(f'Mounting {dev_path}: {command}')
|
|
||||||
|
|
||||||
try:
|
|
||||||
SysCommand(command)
|
|
||||||
except SysCallError as err:
|
|
||||||
raise DiskError(f'Could not mount {dev_path}: {command}\n{err.message}')
|
|
||||||
|
|
||||||
def detect_pre_mounted_mods(self, base_mountpoint: Path) -> list[DeviceModification]:
|
def detect_pre_mounted_mods(self, base_mountpoint: Path) -> list[DeviceModification]:
|
||||||
part_mods: dict[Path, list[PartitionModification]] = {}
|
part_mods: dict[Path, list[PartitionModification]] = {}
|
||||||
|
|
@ -776,7 +561,7 @@ class DeviceHandler:
|
||||||
for device in self.devices:
|
for device in self.devices:
|
||||||
for part_info in device.partition_infos:
|
for part_info in device.partition_infos:
|
||||||
for mountpoint in part_info.mountpoints:
|
for mountpoint in part_info.mountpoints:
|
||||||
if is_subpath(mountpoint, base_mountpoint):
|
if mountpoint.is_relative_to(base_mountpoint):
|
||||||
path = Path(part_info.disk.device.path)
|
path = Path(part_info.disk.device.path)
|
||||||
part_mods.setdefault(path, [])
|
part_mods.setdefault(path, [])
|
||||||
part_mod = PartitionModification.from_existing_partition(part_info)
|
part_mod = PartitionModification.from_existing_partition(part_info)
|
||||||
|
|
@ -814,8 +599,8 @@ class DeviceHandler:
|
||||||
def _wipe(self, dev_path: Path) -> None:
|
def _wipe(self, dev_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Wipe a device (partition or otherwise) of meta-data, be it file system, LVM, etc.
|
Wipe a device (partition or otherwise) of meta-data, be it file system, LVM, etc.
|
||||||
@param dev_path: Device path of the partition to be wiped.
|
@param dev_path: Device path of the partition to be wiped.
|
||||||
@type dev_path: str
|
@type dev_path: str
|
||||||
"""
|
"""
|
||||||
with open(dev_path, 'wb') as p:
|
with open(dev_path, 'wb') as p:
|
||||||
p.write(bytearray(1024))
|
p.write(bytearray(1024))
|
||||||
|
|
@ -837,12 +622,5 @@ class DeviceHandler:
|
||||||
|
|
||||||
self._wipe(block_device.device_info.path)
|
self._wipe(block_device.device_info.path)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def udev_sync() -> None:
|
|
||||||
try:
|
|
||||||
SysCommand('udevadm settle')
|
|
||||||
except SysCallError as err:
|
|
||||||
debug(f'Failed to synchronize with udev: {err}')
|
|
||||||
|
|
||||||
|
|
||||||
device_handler = DeviceHandler()
|
device_handler = DeviceHandler()
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,45 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
from typing import override
|
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.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
|
||||||
from archinstall.lib.models.device import (
|
from archinstall.lib.models.device import (
|
||||||
DEFAULT_ITER_TIME,
|
DEFAULT_ITER_TIME,
|
||||||
|
BDevice,
|
||||||
|
BtrfsMountOption,
|
||||||
BtrfsOptions,
|
BtrfsOptions,
|
||||||
|
DeviceModification,
|
||||||
DiskEncryption,
|
DiskEncryption,
|
||||||
DiskLayoutConfiguration,
|
DiskLayoutConfiguration,
|
||||||
DiskLayoutType,
|
DiskLayoutType,
|
||||||
EncryptionType,
|
EncryptionType,
|
||||||
|
FilesystemType,
|
||||||
LvmConfiguration,
|
LvmConfiguration,
|
||||||
|
LvmLayoutType,
|
||||||
|
LvmVolume,
|
||||||
|
LvmVolumeGroup,
|
||||||
|
ModificationStatus,
|
||||||
|
PartitionFlag,
|
||||||
|
PartitionModification,
|
||||||
|
PartitionType,
|
||||||
|
SectorSize,
|
||||||
|
Size,
|
||||||
SnapshotConfig,
|
SnapshotConfig,
|
||||||
SnapshotType,
|
SnapshotType,
|
||||||
|
SubvolumeModification,
|
||||||
|
Unit,
|
||||||
|
_DeviceInfo,
|
||||||
)
|
)
|
||||||
from archinstall.lib.translationhandler import tr
|
from archinstall.lib.translationhandler import tr
|
||||||
from archinstall.tui.curses_menu import SelectMenu
|
from archinstall.lib.utils.format import as_table
|
||||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||||
from archinstall.tui.result import ResultType
|
from archinstall.tui.result import ResultType
|
||||||
from archinstall.tui.types import Alignment, FrameProperties
|
|
||||||
|
|
||||||
from ..interactions.disk_conf import select_disk_config, select_lvm_config
|
|
||||||
from ..menu.abstract_menu import AbstractSubMenu
|
|
||||||
from ..output import FormattedOutput
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -32,7 +50,7 @@ class DiskMenuConfig:
|
||||||
disk_encryption: DiskEncryption | None
|
disk_encryption: DiskEncryption | None
|
||||||
|
|
||||||
|
|
||||||
class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
|
class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskMenuConfig]):
|
||||||
def __init__(self, disk_layout_config: DiskLayoutConfiguration | None):
|
def __init__(self, disk_layout_config: DiskLayoutConfiguration | None):
|
||||||
if not disk_layout_config:
|
if not disk_layout_config:
|
||||||
self._disk_menu_config = DiskMenuConfig(
|
self._disk_menu_config = DiskMenuConfig(
|
||||||
|
|
@ -51,8 +69,8 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
|
||||||
btrfs_snapshot_config=snapshot_config,
|
btrfs_snapshot_config=snapshot_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
menu_optioons = self._define_menu_options()
|
menu_options = self._define_menu_options()
|
||||||
self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True)
|
self._item_group = MenuItemGroup(menu_options, sort_items=False, checkmarks=True)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
self._item_group,
|
self._item_group,
|
||||||
|
|
@ -95,14 +113,16 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
|
||||||
]
|
]
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def run(self, additional_title: str | None = None) -> DiskLayoutConfiguration | None:
|
async def show(self) -> DiskLayoutConfiguration | None: # type: ignore[override]
|
||||||
super().run(additional_title=additional_title)
|
config: DiskMenuConfig | None = await super().show()
|
||||||
|
if config is None:
|
||||||
|
return None
|
||||||
|
|
||||||
if self._disk_menu_config.disk_config:
|
if config.disk_config:
|
||||||
self._disk_menu_config.disk_config.lvm_config = self._disk_menu_config.lvm_config
|
config.disk_config.lvm_config = self._disk_menu_config.lvm_config
|
||||||
self._disk_menu_config.disk_config.btrfs_options = BtrfsOptions(snapshot_config=self._disk_menu_config.btrfs_snapshot_config)
|
config.disk_config.btrfs_options = BtrfsOptions(snapshot_config=self._disk_menu_config.btrfs_snapshot_config)
|
||||||
self._disk_menu_config.disk_config.disk_encryption = self._disk_menu_config.disk_encryption
|
config.disk_config.disk_encryption = self._disk_menu_config.disk_encryption
|
||||||
return self._disk_menu_config.disk_config
|
return config.disk_config
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -122,7 +142,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _select_disk_encryption(self, preset: DiskEncryption | None) -> DiskEncryption | None:
|
async def _select_disk_encryption(self, preset: DiskEncryption | None) -> DiskEncryption | None:
|
||||||
disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value
|
disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value
|
||||||
lvm_config: LvmConfiguration | None = self._item_group.find_by_key('lvm_config').value
|
lvm_config: LvmConfiguration | None = self._item_group.find_by_key('lvm_config').value
|
||||||
|
|
||||||
|
|
@ -134,12 +154,12 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
|
||||||
if not DiskEncryption.validate_enc(modifications, lvm_config):
|
if not DiskEncryption.validate_enc(modifications, lvm_config):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
disk_encryption = DiskEncryptionMenu(modifications, lvm_config=lvm_config, preset=preset).run()
|
disk_encryption = await DiskEncryptionMenu(modifications, lvm_config=lvm_config, preset=preset).show()
|
||||||
|
|
||||||
return disk_encryption
|
return disk_encryption
|
||||||
|
|
||||||
def _select_disk_layout_config(self, preset: DiskLayoutConfiguration | None) -> DiskLayoutConfiguration | None:
|
async def _select_disk_layout_config(self, preset: DiskLayoutConfiguration | None) -> DiskLayoutConfiguration | None:
|
||||||
disk_config = select_disk_config(preset)
|
disk_config = await select_disk_config(preset)
|
||||||
|
|
||||||
if disk_config != preset:
|
if disk_config != preset:
|
||||||
self._menu_item_group.find_by_key('lvm_config').value = None
|
self._menu_item_group.find_by_key('lvm_config').value = None
|
||||||
|
|
@ -147,20 +167,20 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
|
||||||
|
|
||||||
return disk_config
|
return disk_config
|
||||||
|
|
||||||
def _select_lvm_config(self, preset: LvmConfiguration | None) -> LvmConfiguration | None:
|
async def _select_lvm_config(self, preset: LvmConfiguration | None) -> LvmConfiguration | None:
|
||||||
disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value
|
disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value
|
||||||
|
|
||||||
if not disk_config:
|
if not disk_config:
|
||||||
return preset
|
return preset
|
||||||
|
|
||||||
lvm_config = select_lvm_config(disk_config, preset=preset)
|
lvm_config = await select_lvm_config(disk_config, preset=preset)
|
||||||
|
|
||||||
if lvm_config != preset:
|
if lvm_config != preset:
|
||||||
self._menu_item_group.find_by_key('disk_encryption').value = None
|
self._menu_item_group.find_by_key('disk_encryption').value = None
|
||||||
|
|
||||||
return lvm_config
|
return lvm_config
|
||||||
|
|
||||||
def _select_btrfs_snapshots(self, preset: SnapshotConfig | None) -> SnapshotConfig | None:
|
async def _select_btrfs_snapshots(self, preset: SnapshotConfig | None) -> SnapshotConfig | None:
|
||||||
preset_type = preset.snapshot_type if preset else None
|
preset_type = preset.snapshot_type if preset else None
|
||||||
|
|
||||||
group = MenuItemGroup.from_enum(
|
group = MenuItemGroup.from_enum(
|
||||||
|
|
@ -169,13 +189,11 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
|
||||||
preset=preset_type,
|
preset=preset_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = SelectMenu[SnapshotType](
|
result = await Selection[SnapshotType](
|
||||||
group,
|
group,
|
||||||
allow_reset=True,
|
allow_reset=True,
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
frame=FrameProperties.min(tr('Snapshot type')),
|
).show()
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
match result.type_:
|
||||||
case ResultType.Skip:
|
case ResultType.Skip:
|
||||||
|
|
@ -204,7 +222,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
|
||||||
|
|
||||||
for mod in device_mods:
|
for mod in device_mods:
|
||||||
# create partition table
|
# 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 += f'{mod.device_path}: {mod.device.device_info.model}\n'
|
||||||
output_partition += '{}: {}\n'.format(tr('Wipe'), mod.wipe)
|
output_partition += '{}: {}\n'.format(tr('Wipe'), mod.wipe)
|
||||||
|
|
@ -213,7 +231,7 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
|
||||||
# create btrfs table
|
# create btrfs table
|
||||||
btrfs_partitions = [p for p in mod.partitions if p.btrfs_subvols]
|
btrfs_partitions = [p for p in mod.partitions if p.btrfs_subvols]
|
||||||
for partition in btrfs_partitions:
|
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
|
output = output_partition + output_btrfs
|
||||||
return output.rstrip()
|
return output.rstrip()
|
||||||
|
|
@ -229,12 +247,12 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
|
||||||
output = '{}: {}\n'.format(tr('Configuration'), lvm_config.config_type.display_msg())
|
output = '{}: {}\n'.format(tr('Configuration'), lvm_config.config_type.display_msg())
|
||||||
|
|
||||||
for vol_gp in lvm_config.vol_groups:
|
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 += '{}:\n{}'.format(tr('Physical volumes'), pv_table)
|
||||||
|
|
||||||
output += f'\nVolume Group: {vol_gp.name}'
|
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)
|
output += '\n\n{}:\n{}'.format(tr('Volumes'), lvm_volumes)
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
@ -250,19 +268,20 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
|
||||||
|
|
||||||
def _prev_disk_encryption(self, item: MenuItem) -> str | None:
|
def _prev_disk_encryption(self, item: MenuItem) -> str | None:
|
||||||
disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value
|
disk_config: DiskLayoutConfiguration | None = self._item_group.find_by_key('disk_config').value
|
||||||
|
lvm_config: LvmConfiguration | None = self._item_group.find_by_key('lvm_config').value
|
||||||
enc_config: DiskEncryption | None = item.value
|
enc_config: DiskEncryption | None = item.value
|
||||||
|
|
||||||
if disk_config and not DiskEncryption.validate_enc(disk_config.device_modifications, disk_config.lvm_config):
|
if disk_config and not DiskEncryption.validate_enc(disk_config.device_modifications, lvm_config):
|
||||||
return tr('LVM disk encryption with more than 2 partitions is currently not supported')
|
return tr('LVM disk encryption with more than 2 partitions is currently not supported')
|
||||||
|
|
||||||
if enc_config:
|
if enc_config:
|
||||||
enc_type = enc_config.encryption_type
|
enc_type = enc_config.encryption_type
|
||||||
output = tr('Encryption type') + f': {EncryptionType.type_to_text(enc_type)}\n'
|
output = tr('Encryption type') + f': {enc_type.type_to_text()}\n'
|
||||||
|
|
||||||
if enc_config.encryption_password:
|
if enc_config.encryption_password:
|
||||||
output += tr('Password') + f': {enc_config.encryption_password.hidden()}\n'
|
output += tr('Password') + f': {enc_config.encryption_password.hidden()}\n'
|
||||||
|
|
||||||
if enc_type != EncryptionType.NoEncryption:
|
if enc_type != EncryptionType.NO_ENCRYPTION:
|
||||||
output += tr('Iteration time') + f': {enc_config.iter_time or DEFAULT_ITER_TIME}ms\n'
|
output += tr('Iteration time') + f': {enc_config.iter_time or DEFAULT_ITER_TIME}ms\n'
|
||||||
|
|
||||||
if enc_config.partitions:
|
if enc_config.partitions:
|
||||||
|
|
@ -276,3 +295,582 @@ class DiskLayoutConfigurationMenu(AbstractSubMenu[DiskLayoutConfiguration]):
|
||||||
return output
|
return output
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def select_devices(preset: list[BDevice] | None = []) -> list[BDevice] | None:
|
||||||
|
def _preview_device_selection(item: MenuItem) -> str | None:
|
||||||
|
device: _DeviceInfo = item.value # type: ignore[assignment]
|
||||||
|
dev = device_handler.get_device(device.path)
|
||||||
|
|
||||||
|
if dev and dev.partition_infos:
|
||||||
|
return as_table(dev.partition_infos)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if preset is None:
|
||||||
|
preset = []
|
||||||
|
|
||||||
|
devices = device_handler.devices
|
||||||
|
|
||||||
|
items = [
|
||||||
|
MenuItem(
|
||||||
|
str(d.device_info.path),
|
||||||
|
d.device_info,
|
||||||
|
preview_action=_preview_device_selection,
|
||||||
|
)
|
||||||
|
for d in devices
|
||||||
|
]
|
||||||
|
|
||||||
|
presets = [p.device_info for p in preset]
|
||||||
|
|
||||||
|
group = MenuItemGroup(items)
|
||||||
|
group.set_selected_by_value(presets)
|
||||||
|
|
||||||
|
result = await Table[_DeviceInfo](
|
||||||
|
header=tr('Select disks for the installation'),
|
||||||
|
group=group,
|
||||||
|
presets=presets,
|
||||||
|
allow_skip=True,
|
||||||
|
multi=True,
|
||||||
|
preview_location='bottom',
|
||||||
|
preview_header=tr('Partitions'),
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Reset:
|
||||||
|
return None
|
||||||
|
case ResultType.Skip:
|
||||||
|
return None
|
||||||
|
case ResultType.Selection:
|
||||||
|
selected_device_info = result.get_values()
|
||||||
|
selected_devices = []
|
||||||
|
|
||||||
|
for device in devices:
|
||||||
|
if device.device_info in selected_device_info:
|
||||||
|
selected_devices.append(device)
|
||||||
|
|
||||||
|
return selected_devices
|
||||||
|
|
||||||
|
|
||||||
|
async def get_default_partition_layout(
|
||||||
|
devices: list[BDevice],
|
||||||
|
filesystem_type: FilesystemType | None = None,
|
||||||
|
) -> list[DeviceModification]:
|
||||||
|
if len(devices) == 1:
|
||||||
|
device_modification = await suggest_single_disk_layout(
|
||||||
|
devices[0],
|
||||||
|
filesystem_type=filesystem_type,
|
||||||
|
)
|
||||||
|
return [device_modification]
|
||||||
|
else:
|
||||||
|
return await suggest_multi_disk_layout(
|
||||||
|
devices,
|
||||||
|
filesystem_type=filesystem_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _manual_partitioning(
|
||||||
|
preset: list[DeviceModification],
|
||||||
|
devices: list[BDevice],
|
||||||
|
) -> list[DeviceModification] | None:
|
||||||
|
modifications: list[DeviceModification] = []
|
||||||
|
|
||||||
|
for device in devices:
|
||||||
|
mod = next(filter(lambda x: x.device == device, preset), None)
|
||||||
|
if not mod:
|
||||||
|
mod = DeviceModification(device, wipe=False)
|
||||||
|
|
||||||
|
device_mod = await manual_partitioning(mod, device_handler.partition_table)
|
||||||
|
|
||||||
|
if not device_mod:
|
||||||
|
return None
|
||||||
|
|
||||||
|
modifications.append(device_mod)
|
||||||
|
|
||||||
|
return modifications
|
||||||
|
|
||||||
|
|
||||||
|
async def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLayoutConfiguration | None:
|
||||||
|
default_layout = DiskLayoutType.Default.display_msg()
|
||||||
|
manual_mode = DiskLayoutType.Manual.display_msg()
|
||||||
|
pre_mount_mode = DiskLayoutType.Pre_mount.display_msg()
|
||||||
|
|
||||||
|
items = [
|
||||||
|
MenuItem(default_layout, value=default_layout),
|
||||||
|
MenuItem(manual_mode, value=manual_mode),
|
||||||
|
MenuItem(pre_mount_mode, value=pre_mount_mode),
|
||||||
|
]
|
||||||
|
group = MenuItemGroup(items, sort_items=False)
|
||||||
|
|
||||||
|
if preset:
|
||||||
|
group.set_selected_by_value(preset.config_type.display_msg())
|
||||||
|
|
||||||
|
result = await Selection[str](
|
||||||
|
group,
|
||||||
|
header=tr('Select a disk configuration'),
|
||||||
|
allow_skip=True,
|
||||||
|
allow_reset=True,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case ResultType.Reset:
|
||||||
|
return None
|
||||||
|
case ResultType.Selection:
|
||||||
|
selection = result.get_value()
|
||||||
|
|
||||||
|
if selection == pre_mount_mode:
|
||||||
|
output = tr('Enter root mount directory') + '\n\n'
|
||||||
|
output += tr('You will use whatever drive-setup is mounted at the specified directory') + '\n'
|
||||||
|
output += tr("WARNING: Archinstall won't check the suitability of this setup")
|
||||||
|
|
||||||
|
path = await prompt_dir(output, allow_skip=True)
|
||||||
|
|
||||||
|
if path is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
mods = device_handler.detect_pre_mounted_mods(path)
|
||||||
|
|
||||||
|
return DiskLayoutConfiguration(
|
||||||
|
config_type=DiskLayoutType.Pre_mount,
|
||||||
|
device_modifications=mods,
|
||||||
|
mountpoint=path,
|
||||||
|
)
|
||||||
|
|
||||||
|
preset_devices = [mod.device for mod in preset.device_modifications] if preset else []
|
||||||
|
devices = await select_devices(preset_devices)
|
||||||
|
|
||||||
|
if devices is None:
|
||||||
|
return preset
|
||||||
|
|
||||||
|
if result.get_value() == default_layout:
|
||||||
|
modifications = await get_default_partition_layout(devices)
|
||||||
|
if modifications:
|
||||||
|
return DiskLayoutConfiguration(
|
||||||
|
config_type=DiskLayoutType.Default,
|
||||||
|
device_modifications=modifications,
|
||||||
|
)
|
||||||
|
elif result.get_value() == manual_mode:
|
||||||
|
preset_mods = preset.device_modifications if preset else []
|
||||||
|
partitions = await _manual_partitioning(preset_mods, devices)
|
||||||
|
|
||||||
|
if not partitions:
|
||||||
|
return preset
|
||||||
|
|
||||||
|
return DiskLayoutConfiguration(
|
||||||
|
config_type=DiskLayoutType.Manual,
|
||||||
|
device_modifications=partitions,
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def select_lvm_config(
|
||||||
|
disk_config: DiskLayoutConfiguration,
|
||||||
|
preset: LvmConfiguration | None = None,
|
||||||
|
) -> LvmConfiguration | None:
|
||||||
|
preset_value = preset.config_type.display_msg() if preset else None
|
||||||
|
default_mode = LvmLayoutType.Default.display_msg()
|
||||||
|
|
||||||
|
items = [MenuItem(default_mode, value=default_mode)]
|
||||||
|
group = MenuItemGroup(items)
|
||||||
|
group.set_focus_by_value(preset_value)
|
||||||
|
|
||||||
|
result = await Selection[str](
|
||||||
|
group,
|
||||||
|
allow_reset=True,
|
||||||
|
allow_skip=True,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case ResultType.Reset:
|
||||||
|
return None
|
||||||
|
case ResultType.Selection:
|
||||||
|
if result.get_value() == default_mode:
|
||||||
|
return await suggest_lvm_layout(disk_config)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _boot_partition(sector_size: SectorSize, using_gpt: bool) -> PartitionModification:
|
||||||
|
flags = [PartitionFlag.BOOT]
|
||||||
|
size = Size(1, Unit.GiB, sector_size)
|
||||||
|
start = Size(1, Unit.MiB, sector_size)
|
||||||
|
if using_gpt:
|
||||||
|
flags.append(PartitionFlag.ESP)
|
||||||
|
|
||||||
|
# boot partition
|
||||||
|
return PartitionModification(
|
||||||
|
status=ModificationStatus.CREATE,
|
||||||
|
type=PartitionType.PRIMARY,
|
||||||
|
start=start,
|
||||||
|
length=size,
|
||||||
|
mountpoint=Path('/boot'),
|
||||||
|
fs_type=FilesystemType.FAT32,
|
||||||
|
flags=flags,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def select_main_filesystem_format() -> FilesystemType:
|
||||||
|
items = [
|
||||||
|
MenuItem(FilesystemType.BTRFS.value, value=FilesystemType.BTRFS),
|
||||||
|
MenuItem(FilesystemType.EXT4.value, value=FilesystemType.EXT4),
|
||||||
|
MenuItem(FilesystemType.XFS.value, value=FilesystemType.XFS),
|
||||||
|
MenuItem(FilesystemType.F2FS.value, value=FilesystemType.F2FS),
|
||||||
|
]
|
||||||
|
|
||||||
|
group = MenuItemGroup(items, sort_items=False)
|
||||||
|
result = await Selection[FilesystemType](
|
||||||
|
group,
|
||||||
|
header=tr('Select main filesystem'),
|
||||||
|
allow_skip=False,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Selection:
|
||||||
|
return result.get_value()
|
||||||
|
case _:
|
||||||
|
raise ValueError('Unhandled result type')
|
||||||
|
|
||||||
|
|
||||||
|
async def select_mount_options() -> list[str]:
|
||||||
|
prompt = tr('Would you like to use compression or disable CoW?') + '\n'
|
||||||
|
compression = tr('Use compression')
|
||||||
|
disable_cow = tr('Disable Copy-on-Write')
|
||||||
|
|
||||||
|
items = [
|
||||||
|
MenuItem(compression, value=BtrfsMountOption.compress.value),
|
||||||
|
MenuItem(disable_cow, value=BtrfsMountOption.nodatacow.value),
|
||||||
|
]
|
||||||
|
group = MenuItemGroup(items, sort_items=False)
|
||||||
|
|
||||||
|
result = await Selection[str](
|
||||||
|
group,
|
||||||
|
header=prompt,
|
||||||
|
allow_skip=True,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return []
|
||||||
|
case ResultType.Selection:
|
||||||
|
return [result.get_value()]
|
||||||
|
case _:
|
||||||
|
raise ValueError('Unhandled result type')
|
||||||
|
|
||||||
|
|
||||||
|
def process_root_partition_size(total_size: Size, sector_size: SectorSize) -> Size:
|
||||||
|
# root partition size processing
|
||||||
|
total_device_size = total_size.convert(Unit.GiB)
|
||||||
|
if total_device_size.value > 500:
|
||||||
|
# maximum size
|
||||||
|
return Size(value=50, unit=Unit.GiB, sector_size=sector_size)
|
||||||
|
elif total_device_size.value < 320:
|
||||||
|
# minimum size
|
||||||
|
return Size(value=32, unit=Unit.GiB, sector_size=sector_size)
|
||||||
|
else:
|
||||||
|
# 10% of total size
|
||||||
|
length = total_device_size.value // 10
|
||||||
|
return Size(value=length, unit=Unit.GiB, sector_size=sector_size)
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_btrfs_subvols() -> list[SubvolumeModification]:
|
||||||
|
# https://btrfs.wiki.kernel.org/index.php/FAQ
|
||||||
|
# https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash
|
||||||
|
# https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh
|
||||||
|
return [
|
||||||
|
SubvolumeModification(Path('@'), Path('/')),
|
||||||
|
SubvolumeModification(Path('@home'), Path('/home')),
|
||||||
|
SubvolumeModification(Path('@log'), Path('/var/log')),
|
||||||
|
SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def suggest_single_disk_layout(
|
||||||
|
device: BDevice,
|
||||||
|
filesystem_type: FilesystemType | None = None,
|
||||||
|
separate_home: bool | None = None,
|
||||||
|
) -> DeviceModification:
|
||||||
|
if not filesystem_type:
|
||||||
|
filesystem_type = await select_main_filesystem_format()
|
||||||
|
|
||||||
|
sector_size = device.device_info.sector_size
|
||||||
|
total_size = device.device_info.total_size
|
||||||
|
available_space = total_size
|
||||||
|
min_size_to_allow_home_part = Size(64, Unit.GiB, sector_size)
|
||||||
|
|
||||||
|
if filesystem_type == FilesystemType.BTRFS:
|
||||||
|
prompt = tr('Would you like to use BTRFS subvolumes with a default structure?') + '\n'
|
||||||
|
|
||||||
|
result = await Confirmation(
|
||||||
|
header=prompt,
|
||||||
|
allow_skip=False,
|
||||||
|
preset=True,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
using_subvolumes = result.item() == MenuItem.yes()
|
||||||
|
mount_options = await select_mount_options()
|
||||||
|
else:
|
||||||
|
using_subvolumes = False
|
||||||
|
mount_options = []
|
||||||
|
|
||||||
|
device_modification = DeviceModification(device, wipe=True)
|
||||||
|
|
||||||
|
using_gpt = device_handler.partition_table.is_gpt()
|
||||||
|
|
||||||
|
if using_gpt:
|
||||||
|
available_space = available_space.gpt_end()
|
||||||
|
|
||||||
|
available_space = available_space.align()
|
||||||
|
|
||||||
|
# Used for reference: https://wiki.archlinux.org/title/partitioning
|
||||||
|
|
||||||
|
boot_partition = _boot_partition(sector_size, using_gpt)
|
||||||
|
device_modification.add_partition(boot_partition)
|
||||||
|
|
||||||
|
if separate_home is False or using_subvolumes or total_size < min_size_to_allow_home_part:
|
||||||
|
using_home_partition = False
|
||||||
|
elif separate_home:
|
||||||
|
using_home_partition = True
|
||||||
|
else:
|
||||||
|
prompt = tr('Would you like to create a separate partition for /home?') + '\n'
|
||||||
|
|
||||||
|
result = await Confirmation(
|
||||||
|
header=prompt,
|
||||||
|
allow_skip=False,
|
||||||
|
preset=True,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
using_home_partition = result.item() == MenuItem.yes()
|
||||||
|
|
||||||
|
# root partition
|
||||||
|
root_start = boot_partition.start + boot_partition.length
|
||||||
|
|
||||||
|
# Set a size for / (/root)
|
||||||
|
if using_home_partition:
|
||||||
|
root_length = process_root_partition_size(total_size, sector_size)
|
||||||
|
else:
|
||||||
|
root_length = available_space - root_start
|
||||||
|
|
||||||
|
root_partition = PartitionModification(
|
||||||
|
status=ModificationStatus.CREATE,
|
||||||
|
type=PartitionType.PRIMARY,
|
||||||
|
start=root_start,
|
||||||
|
length=root_length,
|
||||||
|
mountpoint=Path('/') if not using_subvolumes else None,
|
||||||
|
fs_type=filesystem_type,
|
||||||
|
mount_options=mount_options,
|
||||||
|
)
|
||||||
|
|
||||||
|
device_modification.add_partition(root_partition)
|
||||||
|
|
||||||
|
if using_subvolumes:
|
||||||
|
root_partition.btrfs_subvols = get_default_btrfs_subvols()
|
||||||
|
elif using_home_partition:
|
||||||
|
# If we don't want to use subvolumes,
|
||||||
|
# But we want to be able to reuse data between re-installs..
|
||||||
|
# A second partition for /home would be nice if we have the space for it
|
||||||
|
home_start = root_partition.start + root_partition.length
|
||||||
|
home_length = available_space - home_start
|
||||||
|
|
||||||
|
flags = []
|
||||||
|
if using_gpt:
|
||||||
|
flags.append(PartitionFlag.LINUX_HOME)
|
||||||
|
|
||||||
|
home_partition = PartitionModification(
|
||||||
|
status=ModificationStatus.CREATE,
|
||||||
|
type=PartitionType.PRIMARY,
|
||||||
|
start=home_start,
|
||||||
|
length=home_length,
|
||||||
|
mountpoint=Path('/home'),
|
||||||
|
fs_type=filesystem_type,
|
||||||
|
mount_options=mount_options,
|
||||||
|
flags=flags,
|
||||||
|
)
|
||||||
|
device_modification.add_partition(home_partition)
|
||||||
|
|
||||||
|
return device_modification
|
||||||
|
|
||||||
|
|
||||||
|
async def suggest_multi_disk_layout(
|
||||||
|
devices: list[BDevice],
|
||||||
|
filesystem_type: FilesystemType | None = None,
|
||||||
|
) -> list[DeviceModification]:
|
||||||
|
if not devices:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Not really a rock solid foundation of information to stand on, but it's a start:
|
||||||
|
# https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/
|
||||||
|
# https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/
|
||||||
|
min_home_partition_size = Size(40, Unit.GiB, SectorSize.default())
|
||||||
|
# rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size?
|
||||||
|
desired_root_partition_size = Size(32, Unit.GiB, SectorSize.default())
|
||||||
|
mount_options = []
|
||||||
|
|
||||||
|
if not filesystem_type:
|
||||||
|
filesystem_type = await select_main_filesystem_format()
|
||||||
|
|
||||||
|
# find proper disk for /home
|
||||||
|
possible_devices = [d for d in devices if d.device_info.total_size >= min_home_partition_size]
|
||||||
|
home_device = max(possible_devices, key=lambda d: d.device_info.total_size) if possible_devices else None
|
||||||
|
|
||||||
|
# find proper device for /root
|
||||||
|
devices_delta = {}
|
||||||
|
for device in devices:
|
||||||
|
if device is not home_device:
|
||||||
|
delta = device.device_info.total_size - desired_root_partition_size
|
||||||
|
devices_delta[device] = delta
|
||||||
|
|
||||||
|
sorted_delta: list[tuple[BDevice, Size]] = sorted(devices_delta.items(), key=lambda x: x[1])
|
||||||
|
root_device: BDevice | None = sorted_delta[0][0]
|
||||||
|
|
||||||
|
if home_device is None or root_device is None:
|
||||||
|
text = tr('The selected drives do not have the minimum capacity required for an automatic suggestion\n')
|
||||||
|
text += tr('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(Unit.GiB))
|
||||||
|
text += tr('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(Unit.GiB))
|
||||||
|
|
||||||
|
_ = await Notify(text).show()
|
||||||
|
return []
|
||||||
|
|
||||||
|
if filesystem_type == FilesystemType.BTRFS:
|
||||||
|
mount_options = await select_mount_options()
|
||||||
|
|
||||||
|
device_paths = ', '.join(str(d.device_info.path) for d in devices)
|
||||||
|
|
||||||
|
debug(f'Suggesting multi-disk-layout for devices: {device_paths}')
|
||||||
|
debug(f'/root: {root_device.device_info.path}')
|
||||||
|
debug(f'/home: {home_device.device_info.path}')
|
||||||
|
|
||||||
|
root_device_modification = DeviceModification(root_device, wipe=True)
|
||||||
|
home_device_modification = DeviceModification(home_device, wipe=True)
|
||||||
|
|
||||||
|
root_device_sector_size = root_device_modification.device.device_info.sector_size
|
||||||
|
home_device_sector_size = home_device_modification.device.device_info.sector_size
|
||||||
|
|
||||||
|
using_gpt = device_handler.partition_table.is_gpt()
|
||||||
|
|
||||||
|
# add boot partition to the root device
|
||||||
|
boot_partition = _boot_partition(root_device_sector_size, using_gpt)
|
||||||
|
root_device_modification.add_partition(boot_partition)
|
||||||
|
|
||||||
|
root_start = boot_partition.start + boot_partition.length
|
||||||
|
root_length = root_device.device_info.total_size - root_start
|
||||||
|
|
||||||
|
if using_gpt:
|
||||||
|
root_length = root_length.gpt_end()
|
||||||
|
|
||||||
|
root_length = root_length.align()
|
||||||
|
|
||||||
|
# add root partition to the root device
|
||||||
|
root_partition = PartitionModification(
|
||||||
|
status=ModificationStatus.CREATE,
|
||||||
|
type=PartitionType.PRIMARY,
|
||||||
|
start=root_start,
|
||||||
|
length=root_length,
|
||||||
|
mountpoint=Path('/'),
|
||||||
|
mount_options=mount_options,
|
||||||
|
fs_type=filesystem_type,
|
||||||
|
)
|
||||||
|
root_device_modification.add_partition(root_partition)
|
||||||
|
|
||||||
|
home_start = Size(1, Unit.MiB, home_device_sector_size)
|
||||||
|
home_length = home_device.device_info.total_size - home_start
|
||||||
|
|
||||||
|
flags = []
|
||||||
|
if using_gpt:
|
||||||
|
home_length = home_length.gpt_end()
|
||||||
|
flags.append(PartitionFlag.LINUX_HOME)
|
||||||
|
|
||||||
|
home_length = home_length.align()
|
||||||
|
|
||||||
|
# add home partition to home device
|
||||||
|
home_partition = PartitionModification(
|
||||||
|
status=ModificationStatus.CREATE,
|
||||||
|
type=PartitionType.PRIMARY,
|
||||||
|
start=home_start,
|
||||||
|
length=home_length,
|
||||||
|
mountpoint=Path('/home'),
|
||||||
|
mount_options=mount_options,
|
||||||
|
fs_type=filesystem_type,
|
||||||
|
flags=flags,
|
||||||
|
)
|
||||||
|
home_device_modification.add_partition(home_partition)
|
||||||
|
|
||||||
|
return [root_device_modification, home_device_modification]
|
||||||
|
|
||||||
|
|
||||||
|
async def suggest_lvm_layout(
|
||||||
|
disk_config: DiskLayoutConfiguration,
|
||||||
|
filesystem_type: FilesystemType | None = None,
|
||||||
|
vg_grp_name: str = 'ArchinstallVg',
|
||||||
|
) -> LvmConfiguration:
|
||||||
|
if disk_config.config_type != DiskLayoutType.Default:
|
||||||
|
raise ValueError('LVM suggested volumes are only available for default partitioning')
|
||||||
|
|
||||||
|
using_subvolumes = False
|
||||||
|
btrfs_subvols = []
|
||||||
|
home_volume = True
|
||||||
|
mount_options = []
|
||||||
|
|
||||||
|
if not filesystem_type:
|
||||||
|
filesystem_type = await select_main_filesystem_format()
|
||||||
|
|
||||||
|
if filesystem_type == FilesystemType.BTRFS:
|
||||||
|
prompt = tr('Would you like to use BTRFS subvolumes with a default structure?') + '\n'
|
||||||
|
result = await Confirmation(header=prompt, allow_skip=False, preset=True).show()
|
||||||
|
|
||||||
|
using_subvolumes = MenuItem.yes() == result.item()
|
||||||
|
mount_options = await select_mount_options()
|
||||||
|
|
||||||
|
if using_subvolumes:
|
||||||
|
btrfs_subvols = get_default_btrfs_subvols()
|
||||||
|
home_volume = False
|
||||||
|
|
||||||
|
boot_part: PartitionModification | None = None
|
||||||
|
other_part: list[PartitionModification] = []
|
||||||
|
|
||||||
|
for mod in disk_config.device_modifications:
|
||||||
|
for part in mod.partitions:
|
||||||
|
if part.is_boot():
|
||||||
|
boot_part = part
|
||||||
|
else:
|
||||||
|
other_part.append(part)
|
||||||
|
|
||||||
|
if not boot_part:
|
||||||
|
raise ValueError('Unable to find boot partition in partition modifications')
|
||||||
|
|
||||||
|
total_vol_available = sum(
|
||||||
|
[p.length for p in other_part],
|
||||||
|
Size(0, Unit.B, SectorSize.default()),
|
||||||
|
)
|
||||||
|
root_vol_size = process_root_partition_size(total_vol_available, SectorSize.default())
|
||||||
|
home_vol_size = total_vol_available - root_vol_size
|
||||||
|
|
||||||
|
lvm_vol_group = LvmVolumeGroup(vg_grp_name, pvs=other_part)
|
||||||
|
|
||||||
|
root_vol = LvmVolume(
|
||||||
|
status=ModificationStatus.CREATE,
|
||||||
|
name='root',
|
||||||
|
fs_type=filesystem_type,
|
||||||
|
length=root_vol_size,
|
||||||
|
mountpoint=Path('/'),
|
||||||
|
btrfs_subvols=btrfs_subvols,
|
||||||
|
mount_options=mount_options,
|
||||||
|
)
|
||||||
|
|
||||||
|
lvm_vol_group.volumes.append(root_vol)
|
||||||
|
|
||||||
|
if home_volume:
|
||||||
|
home_vol = LvmVolume(
|
||||||
|
status=ModificationStatus.CREATE,
|
||||||
|
name='home',
|
||||||
|
fs_type=filesystem_type,
|
||||||
|
length=home_vol_size,
|
||||||
|
mountpoint=Path('/home'),
|
||||||
|
)
|
||||||
|
|
||||||
|
lvm_vol_group.volumes.append(home_vol)
|
||||||
|
|
||||||
|
return LvmConfiguration(LvmLayoutType.Default, [lvm_vol_group])
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,26 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
|
from archinstall.lib.disk.fido import Fido2
|
||||||
|
from archinstall.lib.menu.abstract_menu import AbstractSubMenu
|
||||||
|
from archinstall.lib.menu.helpers import Input, Selection, Table
|
||||||
from archinstall.lib.menu.menu_helper import MenuHelper
|
from archinstall.lib.menu.menu_helper import MenuHelper
|
||||||
|
from archinstall.lib.menu.util import get_password
|
||||||
from archinstall.lib.models.device import (
|
from archinstall.lib.models.device import (
|
||||||
|
DEFAULT_ITER_TIME,
|
||||||
DeviceModification,
|
DeviceModification,
|
||||||
DiskEncryption,
|
DiskEncryption,
|
||||||
EncryptionType,
|
EncryptionType,
|
||||||
|
Fido2Device,
|
||||||
LvmConfiguration,
|
LvmConfiguration,
|
||||||
LvmVolume,
|
LvmVolume,
|
||||||
PartitionModification,
|
PartitionModification,
|
||||||
)
|
)
|
||||||
|
from archinstall.lib.models.users import Password
|
||||||
from archinstall.lib.translationhandler import tr
|
from archinstall.lib.translationhandler import tr
|
||||||
from archinstall.tui.curses_menu import EditMenu, SelectMenu
|
from archinstall.lib.utils.format import as_table
|
||||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||||
from archinstall.tui.result import ResultType
|
from archinstall.tui.result import ResultType
|
||||||
from archinstall.tui.types import Alignment, FrameProperties
|
|
||||||
|
|
||||||
from ..menu.abstract_menu import AbstractSubMenu
|
|
||||||
from ..models.device import DEFAULT_ITER_TIME, Fido2Device
|
|
||||||
from ..models.users import Password
|
|
||||||
from ..output import FormattedOutput
|
|
||||||
from ..utils.util import get_password
|
|
||||||
from .fido import Fido2
|
|
||||||
|
|
||||||
|
|
||||||
class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
||||||
|
|
@ -39,8 +38,8 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
||||||
self._device_modifications = device_modifications
|
self._device_modifications = device_modifications
|
||||||
self._lvm_config = lvm_config
|
self._lvm_config = lvm_config
|
||||||
|
|
||||||
menu_optioons = self._define_menu_options()
|
menu_options = self._define_menu_options()
|
||||||
self._item_group = MenuItemGroup(menu_optioons, sort_items=False, checkmarks=True)
|
self._item_group = MenuItemGroup(menu_options, sort_items=False, checkmarks=True)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
self._item_group,
|
self._item_group,
|
||||||
|
|
@ -52,9 +51,9 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
||||||
return [
|
return [
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text=tr('Encryption type'),
|
text=tr('Encryption type'),
|
||||||
action=lambda x: select_encryption_type(self._device_modifications, self._lvm_config, x),
|
action=lambda x: select_encryption_type(self._lvm_config, x),
|
||||||
value=self._enc_config.encryption_type,
|
value=self._enc_config.encryption_type,
|
||||||
preview_action=self._preview,
|
preview_action=self._prev_type,
|
||||||
key='encryption_type',
|
key='encryption_type',
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
|
|
@ -62,7 +61,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
||||||
action=lambda x: select_encrypted_password(),
|
action=lambda x: select_encrypted_password(),
|
||||||
value=self._enc_config.encryption_password,
|
value=self._enc_config.encryption_password,
|
||||||
dependencies=[self._check_dep_enc_type],
|
dependencies=[self._check_dep_enc_type],
|
||||||
preview_action=self._preview,
|
preview_action=self._prev_password,
|
||||||
key='encryption_password',
|
key='encryption_password',
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
|
|
@ -70,7 +69,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
||||||
action=select_iteration_time,
|
action=select_iteration_time,
|
||||||
value=self._enc_config.iter_time,
|
value=self._enc_config.iter_time,
|
||||||
dependencies=[self._check_dep_enc_type],
|
dependencies=[self._check_dep_enc_type],
|
||||||
preview_action=self._preview,
|
preview_action=self._prev_iter_time,
|
||||||
key='iter_time',
|
key='iter_time',
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
|
|
@ -78,7 +77,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
||||||
action=lambda x: select_partitions_to_encrypt(self._device_modifications, x),
|
action=lambda x: select_partitions_to_encrypt(self._device_modifications, x),
|
||||||
value=self._enc_config.partitions,
|
value=self._enc_config.partitions,
|
||||||
dependencies=[self._check_dep_partitions],
|
dependencies=[self._check_dep_partitions],
|
||||||
preview_action=self._preview,
|
preview_action=self._prev_partitions,
|
||||||
key='partitions',
|
key='partitions',
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
|
|
@ -86,7 +85,7 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
||||||
action=self._select_lvm_vols,
|
action=self._select_lvm_vols,
|
||||||
value=self._enc_config.lvm_volumes,
|
value=self._enc_config.lvm_volumes,
|
||||||
dependencies=[self._check_dep_lvm_vols],
|
dependencies=[self._check_dep_lvm_vols],
|
||||||
preview_action=self._preview,
|
preview_action=self._prev_lvm_vols,
|
||||||
key='lvm_volumes',
|
key='lvm_volumes',
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
|
|
@ -94,37 +93,39 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
||||||
action=select_hsm,
|
action=select_hsm,
|
||||||
value=self._enc_config.hsm_device,
|
value=self._enc_config.hsm_device,
|
||||||
dependencies=[self._check_dep_enc_type],
|
dependencies=[self._check_dep_enc_type],
|
||||||
preview_action=self._preview,
|
preview_action=self._prev_hsm,
|
||||||
key='hsm_device',
|
key='hsm_device',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def _select_lvm_vols(self, preset: list[LvmVolume]) -> list[LvmVolume]:
|
async def _select_lvm_vols(self, preset: list[LvmVolume]) -> list[LvmVolume]:
|
||||||
if self._lvm_config:
|
if self._lvm_config:
|
||||||
return select_lvm_vols_to_encrypt(self._lvm_config, preset=preset)
|
return await select_lvm_vols_to_encrypt(self._lvm_config, preset=preset)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _check_dep_enc_type(self) -> bool:
|
def _check_dep_enc_type(self) -> bool:
|
||||||
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
|
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
|
||||||
if enc_type and enc_type != EncryptionType.NoEncryption:
|
if enc_type and enc_type != EncryptionType.NO_ENCRYPTION:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _check_dep_partitions(self) -> bool:
|
def _check_dep_partitions(self) -> bool:
|
||||||
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
|
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
|
||||||
if enc_type and enc_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks]:
|
if enc_type and enc_type in [EncryptionType.LUKS, EncryptionType.LVM_ON_LUKS]:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _check_dep_lvm_vols(self) -> bool:
|
def _check_dep_lvm_vols(self) -> bool:
|
||||||
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
|
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
|
||||||
if enc_type and enc_type == EncryptionType.LuksOnLvm:
|
if enc_type and enc_type == EncryptionType.LUKS_ON_LVM:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def run(self, additional_title: str | None = None) -> DiskEncryption | None:
|
async def show(self) -> DiskEncryption | None:
|
||||||
super().run(additional_title=additional_title)
|
enc_config = await super().show()
|
||||||
|
if enc_config is None:
|
||||||
|
return None
|
||||||
|
|
||||||
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
|
enc_type: EncryptionType | None = self._item_group.find_by_key('encryption_type').value
|
||||||
enc_password: Password | None = self._item_group.find_by_key('encryption_password').value
|
enc_password: Password | None = self._item_group.find_by_key('encryption_password').value
|
||||||
|
|
@ -136,19 +137,19 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
||||||
assert enc_partitions is not None
|
assert enc_partitions is not None
|
||||||
assert enc_lvm_vols is not None
|
assert enc_lvm_vols is not None
|
||||||
|
|
||||||
if enc_type in [EncryptionType.Luks, EncryptionType.LvmOnLuks] and enc_partitions:
|
if enc_type in [EncryptionType.LUKS, EncryptionType.LVM_ON_LUKS] and enc_partitions:
|
||||||
enc_lvm_vols = []
|
enc_lvm_vols = []
|
||||||
|
|
||||||
if enc_type == EncryptionType.LuksOnLvm:
|
if enc_type == EncryptionType.LUKS_ON_LVM:
|
||||||
enc_partitions = []
|
enc_partitions = []
|
||||||
|
|
||||||
if enc_type != EncryptionType.NoEncryption and enc_password and (enc_partitions or enc_lvm_vols):
|
if enc_type != EncryptionType.NO_ENCRYPTION and enc_password and (enc_partitions or enc_lvm_vols):
|
||||||
return DiskEncryption(
|
return DiskEncryption(
|
||||||
encryption_password=enc_password,
|
encryption_password=enc_password,
|
||||||
encryption_type=enc_type,
|
encryption_type=enc_type,
|
||||||
partitions=enc_partitions,
|
partitions=enc_partitions,
|
||||||
lvm_volumes=enc_lvm_vols,
|
lvm_volumes=enc_lvm_vols,
|
||||||
hsm_device=self._enc_config.hsm_device,
|
hsm_device=enc_config.hsm_device,
|
||||||
iter_time=iter_time or DEFAULT_ITER_TIME,
|
iter_time=iter_time or DEFAULT_ITER_TIME,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -157,22 +158,22 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
||||||
def _preview(self, item: MenuItem) -> str | None:
|
def _preview(self, item: MenuItem) -> str | None:
|
||||||
output = ''
|
output = ''
|
||||||
|
|
||||||
if (enc_type := self._prev_type()) is not None:
|
if (enc_type := self._prev_type(item)) is not None:
|
||||||
output += enc_type
|
output += enc_type
|
||||||
|
|
||||||
if (enc_pwd := self._prev_password()) is not None:
|
if (enc_pwd := self._prev_password(item)) is not None:
|
||||||
output += f'\n{enc_pwd}'
|
output += f'\n{enc_pwd}'
|
||||||
|
|
||||||
if (iter_time := self._prev_iter_time()) is not None:
|
if (iter_time := self._prev_iter_time(item)) is not None:
|
||||||
output += f'\n{iter_time}'
|
output += f'\n{iter_time}'
|
||||||
|
|
||||||
if (fido_device := self._prev_hsm()) is not None:
|
if (fido_device := self._prev_hsm(item)) is not None:
|
||||||
output += f'\n{fido_device}'
|
output += f'\n{fido_device}'
|
||||||
|
|
||||||
if (partitions := self._prev_partitions()) is not None:
|
if (partitions := self._prev_partitions(item)) is not None:
|
||||||
output += f'\n\n{partitions}'
|
output += f'\n\n{partitions}'
|
||||||
|
|
||||||
if (lvm := self._prev_lvm_vols()) is not None:
|
if (lvm := self._prev_lvm_vols(item)) is not None:
|
||||||
output += f'\n\n{lvm}'
|
output += f'\n\n{lvm}'
|
||||||
|
|
||||||
if not output:
|
if not output:
|
||||||
|
|
@ -180,91 +181,84 @@ class DiskEncryptionMenu(AbstractSubMenu[DiskEncryption]):
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def _prev_type(self) -> str | None:
|
def _prev_type(self, item: MenuItem) -> str | None:
|
||||||
enc_type = self._item_group.find_by_key('encryption_type').value
|
enc_type = self._item_group.find_by_key('encryption_type').value
|
||||||
|
|
||||||
if enc_type:
|
if enc_type:
|
||||||
enc_text = EncryptionType.type_to_text(enc_type)
|
enc_text = enc_type.type_to_text()
|
||||||
return f'{tr("Encryption type")}: {enc_text}'
|
return f'{tr("Encryption type")}: {enc_text}'
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _prev_password(self) -> str | None:
|
def _prev_password(self, item: MenuItem) -> str | None:
|
||||||
enc_pwd = self._item_group.find_by_key('encryption_password').value
|
if item.value:
|
||||||
|
return f'{tr("Encryption password")}: {item.value.hidden()}'
|
||||||
if enc_pwd:
|
|
||||||
return f'{tr("Encryption password")}: {enc_pwd.hidden()}'
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _prev_partitions(self) -> str | None:
|
def _prev_partitions(self, item: MenuItem) -> str | None:
|
||||||
partitions: list[PartitionModification] | None = self._item_group.find_by_key('partitions').value
|
if item.value:
|
||||||
|
|
||||||
if partitions:
|
|
||||||
output = tr('Partitions to be encrypted') + '\n'
|
output = tr('Partitions to be encrypted') + '\n'
|
||||||
output += FormattedOutput.as_table(partitions)
|
output += as_table(item.value)
|
||||||
return output.rstrip()
|
return output.rstrip()
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _prev_lvm_vols(self) -> str | None:
|
def _prev_lvm_vols(self, item: MenuItem) -> str | None:
|
||||||
volumes: list[PartitionModification] | None = self._item_group.find_by_key('lvm_volumes').value
|
if item.value:
|
||||||
|
|
||||||
if volumes:
|
|
||||||
output = tr('LVM volumes to be encrypted') + '\n'
|
output = tr('LVM volumes to be encrypted') + '\n'
|
||||||
output += FormattedOutput.as_table(volumes)
|
output += as_table(item.value)
|
||||||
return output.rstrip()
|
return output.rstrip()
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _prev_hsm(self) -> str | None:
|
def _prev_hsm(self, item: MenuItem) -> str | None:
|
||||||
fido_device: Fido2Device | None = self._item_group.find_by_key('hsm_device').value
|
if not item.value:
|
||||||
|
|
||||||
if not fido_device:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
fido_device: Fido2Device = item.value
|
||||||
|
|
||||||
output = str(fido_device.path)
|
output = str(fido_device.path)
|
||||||
output += f' ({fido_device.manufacturer}, {fido_device.product})'
|
output += f' ({fido_device.manufacturer}, {fido_device.product})'
|
||||||
return f'{tr("HSM device")}: {output}'
|
return f'{tr("HSM device")}: {output}'
|
||||||
|
|
||||||
def _prev_iter_time(self) -> str | None:
|
def _prev_iter_time(self, item: MenuItem) -> str | None:
|
||||||
iter_time = self._item_group.find_by_key('iter_time').value
|
if item.value:
|
||||||
enc_type = self._item_group.find_by_key('encryption_type').value
|
iter_time = item.value
|
||||||
|
enc_type = self._item_group.find_by_key('encryption_type').value
|
||||||
|
|
||||||
if iter_time and enc_type != EncryptionType.NoEncryption:
|
if iter_time and enc_type != EncryptionType.NO_ENCRYPTION:
|
||||||
return f'{tr("Iteration time")}: {iter_time}ms'
|
return f'{tr("Iteration time")}: {iter_time}ms'
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def select_encryption_type(
|
async def select_encryption_type(
|
||||||
device_modifications: list[DeviceModification],
|
|
||||||
lvm_config: LvmConfiguration | None = None,
|
lvm_config: LvmConfiguration | None = None,
|
||||||
preset: EncryptionType | None = None,
|
preset: EncryptionType | None = None,
|
||||||
) -> EncryptionType | None:
|
) -> EncryptionType | None:
|
||||||
options: list[EncryptionType] = []
|
options: list[EncryptionType] = []
|
||||||
|
|
||||||
if lvm_config:
|
if lvm_config:
|
||||||
options = [EncryptionType.LvmOnLuks, EncryptionType.LuksOnLvm]
|
options = [EncryptionType.LVM_ON_LUKS, EncryptionType.LUKS_ON_LVM]
|
||||||
else:
|
else:
|
||||||
options = [EncryptionType.Luks]
|
options = [EncryptionType.LUKS]
|
||||||
|
|
||||||
if not preset:
|
if not preset:
|
||||||
preset = options[0]
|
preset = options[0]
|
||||||
|
|
||||||
preset_value = EncryptionType.type_to_text(preset)
|
preset_value = preset.type_to_text()
|
||||||
|
|
||||||
items = [MenuItem(EncryptionType.type_to_text(o), value=o) for o in options]
|
items = [MenuItem(o.type_to_text(), value=o) for o in options]
|
||||||
group = MenuItemGroup(items)
|
group = MenuItemGroup(items)
|
||||||
group.set_focus_by_value(preset_value)
|
group.set_focus_by_value(preset_value)
|
||||||
|
|
||||||
result = SelectMenu[EncryptionType](
|
result = await Selection[EncryptionType](
|
||||||
group,
|
group,
|
||||||
|
header=tr('Select encryption type'),
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
allow_reset=True,
|
allow_reset=True,
|
||||||
alignment=Alignment.CENTER,
|
).show()
|
||||||
frame=FrameProperties.min(tr('Encryption type')),
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
match result.type_:
|
||||||
case ResultType.Reset:
|
case ResultType.Reset:
|
||||||
|
|
@ -275,10 +269,9 @@ def select_encryption_type(
|
||||||
return result.get_value()
|
return result.get_value()
|
||||||
|
|
||||||
|
|
||||||
def select_encrypted_password() -> Password | None:
|
async def select_encrypted_password() -> Password | None:
|
||||||
header = tr('Enter disk encryption password (leave blank for no encryption)') + '\n'
|
header = tr('Enter disk encryption password (leave blank for no encryption)') + '\n'
|
||||||
password = get_password(
|
password = await get_password(
|
||||||
text=tr('Disk encryption password'),
|
|
||||||
header=header,
|
header=header,
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
)
|
)
|
||||||
|
|
@ -286,7 +279,7 @@ def select_encrypted_password() -> Password | None:
|
||||||
return password
|
return password
|
||||||
|
|
||||||
|
|
||||||
def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None:
|
async def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None:
|
||||||
header = tr('Select a FIDO2 device to use for HSM') + '\n'
|
header = tr('Select a FIDO2 device to use for HSM') + '\n'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -297,12 +290,11 @@ def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None:
|
||||||
if fido_devices:
|
if fido_devices:
|
||||||
group = MenuHelper(data=fido_devices).create_menu_group()
|
group = MenuHelper(data=fido_devices).create_menu_group()
|
||||||
|
|
||||||
result = SelectMenu[Fido2Device](
|
result = await Selection[Fido2Device](
|
||||||
group,
|
group,
|
||||||
header=header,
|
header=header,
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
).run()
|
).show()
|
||||||
|
|
||||||
match result.type_:
|
match result.type_:
|
||||||
case ResultType.Reset:
|
case ResultType.Reset:
|
||||||
|
|
@ -315,7 +307,7 @@ def select_hsm(preset: Fido2Device | None = None) -> Fido2Device | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def select_partitions_to_encrypt(
|
async def select_partitions_to_encrypt(
|
||||||
modification: list[DeviceModification],
|
modification: list[DeviceModification],
|
||||||
preset: list[PartitionModification],
|
preset: list[PartitionModification],
|
||||||
) -> list[PartitionModification]:
|
) -> list[PartitionModification]:
|
||||||
|
|
@ -329,15 +321,15 @@ def select_partitions_to_encrypt(
|
||||||
avail_partitions = [p for p in partitions if not p.exists()]
|
avail_partitions = [p for p in partitions if not p.exists()]
|
||||||
|
|
||||||
if avail_partitions:
|
if avail_partitions:
|
||||||
group = MenuHelper(data=avail_partitions).create_menu_group()
|
group = MenuItemGroup.from_objects(avail_partitions)
|
||||||
group.set_selected_by_value(preset)
|
group.set_selected_by_value(preset)
|
||||||
|
|
||||||
result = SelectMenu[PartitionModification](
|
result = await Table[PartitionModification](
|
||||||
group,
|
header=tr('Select disks for the installation'),
|
||||||
alignment=Alignment.CENTER,
|
group=group,
|
||||||
multi=True,
|
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
).run()
|
multi=True,
|
||||||
|
).show()
|
||||||
|
|
||||||
match result.type_:
|
match result.type_:
|
||||||
case ResultType.Reset:
|
case ResultType.Reset:
|
||||||
|
|
@ -351,20 +343,22 @@ def select_partitions_to_encrypt(
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def select_lvm_vols_to_encrypt(
|
async def select_lvm_vols_to_encrypt(
|
||||||
lvm_config: LvmConfiguration,
|
lvm_config: LvmConfiguration,
|
||||||
preset: list[LvmVolume],
|
preset: list[LvmVolume],
|
||||||
) -> list[LvmVolume]:
|
) -> list[LvmVolume]:
|
||||||
volumes: list[LvmVolume] = lvm_config.get_all_volumes()
|
volumes: list[LvmVolume] = lvm_config.get_all_volumes()
|
||||||
|
|
||||||
if volumes:
|
if volumes:
|
||||||
group = MenuHelper(data=volumes).create_menu_group()
|
group = MenuItemGroup.from_objects(volumes)
|
||||||
|
group.set_selected_by_value(preset)
|
||||||
|
|
||||||
result = SelectMenu[LvmVolume](
|
result = await Table[LvmVolume](
|
||||||
group,
|
header=tr('Select disks for the installation'),
|
||||||
alignment=Alignment.CENTER,
|
group=group,
|
||||||
|
allow_skip=True,
|
||||||
multi=True,
|
multi=True,
|
||||||
).run()
|
).show()
|
||||||
|
|
||||||
match result.type_:
|
match result.type_:
|
||||||
case ResultType.Reset:
|
case ResultType.Reset:
|
||||||
|
|
@ -378,15 +372,12 @@ def select_lvm_vols_to_encrypt(
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def select_iteration_time(preset: int | None = None) -> int | None:
|
async def select_iteration_time(preset: int | None = None) -> int | None:
|
||||||
header = tr('Enter iteration time for LUKS encryption (in milliseconds)') + '\n'
|
header = tr('Enter iteration time for LUKS encryption (in milliseconds)') + '\n'
|
||||||
header += tr('Higher values increase security but slow down boot time') + '\n'
|
header += tr('Higher values increase security but slow down boot time') + '\n'
|
||||||
header += tr(f'Default: {DEFAULT_ITER_TIME}ms, Recommended range: 1000-60000') + '\n'
|
header += tr('Default: {}ms, Recommended range: 1000-60000').format(DEFAULT_ITER_TIME) + '\n'
|
||||||
|
|
||||||
def validate_iter_time(value: str | None) -> str | None:
|
|
||||||
if not value:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
def validate_iter_time(value: str) -> str | None:
|
||||||
try:
|
try:
|
||||||
iter_time = int(value)
|
iter_time = int(value)
|
||||||
if iter_time < 100:
|
if iter_time < 100:
|
||||||
|
|
@ -397,21 +388,19 @@ def select_iteration_time(preset: int | None = None) -> int | None:
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return tr('Please enter a valid number')
|
return tr('Please enter a valid number')
|
||||||
|
|
||||||
result = EditMenu(
|
result = await Input(
|
||||||
tr('Iteration time'),
|
|
||||||
header=header,
|
header=header,
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
default_text=str(preset) if preset else str(DEFAULT_ITER_TIME),
|
default_value=str(preset) if preset else str(DEFAULT_ITER_TIME),
|
||||||
validator=validate_iter_time,
|
validator_callback=validate_iter_time,
|
||||||
).input()
|
).show()
|
||||||
|
|
||||||
match result.type_:
|
match result.type_:
|
||||||
case ResultType.Skip:
|
case ResultType.Skip:
|
||||||
return preset
|
return preset
|
||||||
case ResultType.Selection:
|
case ResultType.Selection:
|
||||||
if not result.text():
|
if not result.get_value():
|
||||||
return preset
|
return preset
|
||||||
return int(result.text())
|
return int(result.get_value())
|
||||||
case ResultType.Reset:
|
case ResultType.Reset:
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import getpass
|
import getpass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import ClassVar
|
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.device import Fido2Device
|
||||||
|
from archinstall.lib.models.users import Password
|
||||||
from ..exceptions import SysCallError
|
from archinstall.lib.utils.encoding import clear_vt100_escape_codes_from_str
|
||||||
from ..general import SysCommand, SysCommandWorker, clear_vt100_escape_codes_from_str
|
|
||||||
from ..models.users import Password
|
|
||||||
from ..output import error, info
|
|
||||||
|
|
||||||
|
|
||||||
class Fido2:
|
class Fido2:
|
||||||
|
|
@ -63,8 +61,8 @@ class Fido2:
|
||||||
|
|
||||||
Output example:
|
Output example:
|
||||||
|
|
||||||
PATH MANUFACTURER PRODUCT
|
PATH MANUFACTURER PRODUCT
|
||||||
/dev/hidraw1 Yubico YubiKey OTP+FIDO+CCID
|
/dev/hidraw1 Yubico YubiKey OTP+FIDO+CCID
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# to prevent continuous reloading which will slow
|
# to prevent continuous reloading which will slow
|
||||||
|
|
@ -101,17 +99,14 @@ class Fido2:
|
||||||
|
|
||||||
return cls._cryptenroll_devices
|
return cls._cryptenroll_devices
|
||||||
|
|
||||||
@classmethod
|
@staticmethod
|
||||||
def fido2_enroll(
|
def fido2_enroll(hsm_device: Fido2Device, dev_path: Path, password: Password) -> None:
|
||||||
cls,
|
|
||||||
hsm_device: Fido2Device,
|
|
||||||
dev_path: Path,
|
|
||||||
password: Password,
|
|
||||||
) -> None:
|
|
||||||
worker = SysCommandWorker(f'systemd-cryptenroll --fido2-device={hsm_device.path} {dev_path}', peek_output=True)
|
worker = SysCommandWorker(f'systemd-cryptenroll --fido2-device={hsm_device.path} {dev_path}', peek_output=True)
|
||||||
pw_inputted = False
|
pw_inputted = False
|
||||||
pin_inputted = False
|
pin_inputted = False
|
||||||
|
|
||||||
|
info('You might need to touch the FIDO2 device to unlock it if no prompt comes up after 3 seconds')
|
||||||
|
|
||||||
while worker.is_alive():
|
while worker.is_alive():
|
||||||
if pw_inputted is False:
|
if pw_inputted is False:
|
||||||
if bytes(f'please enter current passphrase for disk {dev_path}', 'UTF-8') in worker._trace_log.lower():
|
if bytes(f'please enter current passphrase for disk {dev_path}', 'UTF-8') in worker._trace_log.lower():
|
||||||
|
|
@ -121,5 +116,3 @@ class Fido2:
|
||||||
if bytes('please enter security token pin', 'UTF-8') in worker._trace_log.lower():
|
if bytes('please enter security token pin', 'UTF-8') in worker._trace_log.lower():
|
||||||
worker.write(bytes(getpass.getpass(' '), 'UTF-8'))
|
worker.write(bytes(getpass.getpass(' '), 'UTF-8'))
|
||||||
pin_inputted = True
|
pin_inputted = True
|
||||||
|
|
||||||
info('You might need to touch the FIDO2 device to unlock it if no prompt comes up after 3 seconds')
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from archinstall.lib.translationhandler import tr
|
from archinstall.lib.disk.device_handler import device_handler
|
||||||
from archinstall.tui.curses_menu import Tui
|
from archinstall.lib.disk.luks import Luks2
|
||||||
|
from archinstall.lib.disk.lvm import (
|
||||||
from ..interactions.general_conf import ask_abort
|
lvm_group_info,
|
||||||
from ..luks import Luks2
|
lvm_pv_create,
|
||||||
from ..models.device import (
|
lvm_vg_create,
|
||||||
|
lvm_vol_create,
|
||||||
|
lvm_vol_info,
|
||||||
|
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,
|
DiskEncryption,
|
||||||
DiskLayoutConfiguration,
|
DiskLayoutConfiguration,
|
||||||
DiskLayoutType,
|
DiskLayoutType,
|
||||||
|
|
@ -23,8 +28,6 @@ from ..models.device import (
|
||||||
Size,
|
Size,
|
||||||
Unit,
|
Unit,
|
||||||
)
|
)
|
||||||
from ..output import debug, info
|
|
||||||
from .device_handler import device_handler
|
|
||||||
|
|
||||||
|
|
||||||
class FilesystemHandler:
|
class FilesystemHandler:
|
||||||
|
|
@ -32,7 +35,7 @@ class FilesystemHandler:
|
||||||
self._disk_config = disk_config
|
self._disk_config = disk_config
|
||||||
self._enc_config = disk_config.disk_encryption
|
self._enc_config = disk_config.disk_encryption
|
||||||
|
|
||||||
def perform_filesystem_operations(self, show_countdown: bool = True) -> None:
|
def perform_filesystem_operations(self) -> None:
|
||||||
if self._disk_config.config_type == DiskLayoutType.Pre_mount:
|
if self._disk_config.config_type == DiskLayoutType.Pre_mount:
|
||||||
debug('Disk layout configuration is set to pre-mount, not performing any operations')
|
debug('Disk layout configuration is set to pre-mount, not performing any operations')
|
||||||
return
|
return
|
||||||
|
|
@ -43,11 +46,6 @@ class FilesystemHandler:
|
||||||
debug('No modifications required')
|
debug('No modifications required')
|
||||||
return
|
return
|
||||||
|
|
||||||
device_paths = ', '.join([str(mod.device.device_info.path) for mod in device_mods])
|
|
||||||
|
|
||||||
if show_countdown:
|
|
||||||
self._final_warning(device_paths)
|
|
||||||
|
|
||||||
# Setup the blockdevice, filesystem (and optionally encryption).
|
# Setup the blockdevice, filesystem (and optionally encryption).
|
||||||
# Once that's done, we'll hand over to perform_installation()
|
# Once that's done, we'll hand over to perform_installation()
|
||||||
|
|
||||||
|
|
@ -58,7 +56,7 @@ class FilesystemHandler:
|
||||||
for mod in device_mods:
|
for mod in device_mods:
|
||||||
device_handler.partition(mod)
|
device_handler.partition(mod)
|
||||||
|
|
||||||
device_handler.udev_sync()
|
udev_sync()
|
||||||
|
|
||||||
if self._disk_config.lvm_config:
|
if self._disk_config.lvm_config:
|
||||||
for mod in device_mods:
|
for mod in device_mods:
|
||||||
|
|
@ -72,7 +70,7 @@ class FilesystemHandler:
|
||||||
self._format_partitions(mod.partitions)
|
self._format_partitions(mod.partitions)
|
||||||
|
|
||||||
for part_mod in mod.partitions:
|
for part_mod in mod.partitions:
|
||||||
if part_mod.fs_type == FilesystemType.Btrfs and part_mod.is_create_or_modify():
|
if part_mod.fs_type == FilesystemType.BTRFS and part_mod.is_create_or_modify():
|
||||||
device_handler.create_btrfs_volumes(part_mod, enc_conf=self._enc_config)
|
device_handler.create_btrfs_volumes(part_mod, enc_conf=self._enc_config)
|
||||||
|
|
||||||
def _format_partitions(
|
def _format_partitions(
|
||||||
|
|
@ -102,7 +100,7 @@ class FilesystemHandler:
|
||||||
device_handler.format(part_mod.safe_fs_type, part_mod.safe_dev_path)
|
device_handler.format(part_mod.safe_fs_type, part_mod.safe_dev_path)
|
||||||
|
|
||||||
# synchronize with udev before using lsblk
|
# synchronize with udev before using lsblk
|
||||||
device_handler.udev_sync()
|
udev_sync()
|
||||||
|
|
||||||
lsblk_info = device_handler.fetch_part_info(part_mod.safe_dev_path)
|
lsblk_info = device_handler.fetch_part_info(part_mod.safe_dev_path)
|
||||||
|
|
||||||
|
|
@ -115,7 +113,7 @@ class FilesystemHandler:
|
||||||
# verify that all partitions have a path set (which implies that they have been created)
|
# verify that all partitions have a path set (which implies that they have been created)
|
||||||
lambda x: x.dev_path is None: ValueError('When formatting, all partitions must have a path set'),
|
lambda x: x.dev_path is None: ValueError('When formatting, all partitions must have a path set'),
|
||||||
# crypto luks is not a valid file system type
|
# crypto luks is not a valid file system type
|
||||||
lambda x: x.fs_type is FilesystemType.Crypto_luks: ValueError('Crypto luks cannot be set as a filesystem type'),
|
lambda x: x.fs_type is FilesystemType.CRYPTO_LUKS: ValueError('Crypto luks cannot be set as a filesystem type'),
|
||||||
# file system type must be set
|
# file system type must be set
|
||||||
lambda x: x.fs_type is None: ValueError('File system type must be set for modification'),
|
lambda x: x.fs_type is None: ValueError('File system type must be set for modification'),
|
||||||
}
|
}
|
||||||
|
|
@ -141,34 +139,25 @@ class FilesystemHandler:
|
||||||
self._format_lvm_vols(self._disk_config.lvm_config)
|
self._format_lvm_vols(self._disk_config.lvm_config)
|
||||||
|
|
||||||
def _setup_lvm_encrypted(self, lvm_config: LvmConfiguration, enc_config: DiskEncryption) -> None:
|
def _setup_lvm_encrypted(self, lvm_config: LvmConfiguration, enc_config: DiskEncryption) -> None:
|
||||||
if enc_config.encryption_type == EncryptionType.LvmOnLuks:
|
if enc_config.encryption_type == EncryptionType.LVM_ON_LUKS:
|
||||||
enc_mods = self._encrypt_partitions(enc_config, lock_after_create=False)
|
enc_mods = self._encrypt_partitions(enc_config, lock_after_create=False)
|
||||||
|
|
||||||
self._setup_lvm(lvm_config, enc_mods)
|
self._setup_lvm(lvm_config, enc_mods)
|
||||||
self._format_lvm_vols(lvm_config)
|
self._format_lvm_vols(lvm_config)
|
||||||
|
|
||||||
# export the lvm group safely otherwise the Luks cannot be closed
|
# Don't close LVM or LUKS during setup - keep everything active
|
||||||
self._safely_close_lvm(lvm_config)
|
# The installation phase will handle unlocking and mounting
|
||||||
|
# Closing causes "parent leaked" and lvchange errors
|
||||||
for luks in enc_mods.values():
|
elif enc_config.encryption_type == EncryptionType.LUKS_ON_LVM:
|
||||||
luks.lock()
|
|
||||||
elif enc_config.encryption_type == EncryptionType.LuksOnLvm:
|
|
||||||
self._setup_lvm(lvm_config)
|
self._setup_lvm(lvm_config)
|
||||||
enc_vols = self._encrypt_lvm_vols(lvm_config, enc_config, False)
|
enc_vols = self._encrypt_lvm_vols(lvm_config, enc_config, False)
|
||||||
self._format_lvm_vols(lvm_config, enc_vols)
|
self._format_lvm_vols(lvm_config, enc_vols)
|
||||||
|
|
||||||
|
# Lock LUKS devices but keep LVM active
|
||||||
|
# LVM volumes must remain active for later re-unlock during installation
|
||||||
for luks in enc_vols.values():
|
for luks in enc_vols.values():
|
||||||
luks.lock()
|
luks.lock()
|
||||||
|
|
||||||
self._safely_close_lvm(lvm_config)
|
|
||||||
|
|
||||||
def _safely_close_lvm(self, lvm_config: LvmConfiguration) -> None:
|
|
||||||
for vg in lvm_config.vol_groups:
|
|
||||||
for vol in vg.volumes:
|
|
||||||
device_handler.lvm_vol_change(vol, False)
|
|
||||||
|
|
||||||
device_handler.lvm_export_vg(vg)
|
|
||||||
|
|
||||||
def _setup_lvm(
|
def _setup_lvm(
|
||||||
self,
|
self,
|
||||||
lvm_config: LvmConfiguration,
|
lvm_config: LvmConfiguration,
|
||||||
|
|
@ -179,10 +168,10 @@ class FilesystemHandler:
|
||||||
for vg in lvm_config.vol_groups:
|
for vg in lvm_config.vol_groups:
|
||||||
pv_dev_paths = self._get_all_pv_dev_paths(vg.pvs, enc_mods)
|
pv_dev_paths = self._get_all_pv_dev_paths(vg.pvs, enc_mods)
|
||||||
|
|
||||||
device_handler.lvm_vg_create(pv_dev_paths, vg.name)
|
lvm_vg_create(pv_dev_paths, vg.name)
|
||||||
|
|
||||||
# figure out what the actual available size in the group is
|
# figure out what the actual available size in the group is
|
||||||
vg_info = device_handler.lvm_group_info(vg.name)
|
vg_info = lvm_group_info(vg.name)
|
||||||
|
|
||||||
if not vg_info:
|
if not vg_info:
|
||||||
raise ValueError('Unable to fetch VG info')
|
raise ValueError('Unable to fetch VG info')
|
||||||
|
|
@ -212,11 +201,11 @@ class FilesystemHandler:
|
||||||
offset = max_vol_offset if lv == max_vol else None
|
offset = max_vol_offset if lv == max_vol else None
|
||||||
|
|
||||||
debug(f'vg: {vg.name}, vol: {lv.name}, offset: {offset}')
|
debug(f'vg: {vg.name}, vol: {lv.name}, offset: {offset}')
|
||||||
device_handler.lvm_vol_create(vg.name, lv, offset)
|
lvm_vol_create(vg.name, lv, offset)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
debug('Fetching LVM volume info')
|
debug('Fetching LVM volume info')
|
||||||
lv_info = device_handler.lvm_vol_info(lv.name)
|
lv_info = lvm_vol_info(lv.name)
|
||||||
if lv_info is not None:
|
if lv_info is not None:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -241,7 +230,7 @@ class FilesystemHandler:
|
||||||
# find the mapper device yet
|
# find the mapper device yet
|
||||||
device_handler.format(vol.fs_type, path)
|
device_handler.format(vol.fs_type, path)
|
||||||
|
|
||||||
if vol.fs_type == FilesystemType.Btrfs:
|
if vol.fs_type == FilesystemType.BTRFS:
|
||||||
device_handler.create_lvm_btrfs_subvolumes(path, vol.btrfs_subvols, vol.mount_options)
|
device_handler.create_lvm_btrfs_subvolumes(path, vol.btrfs_subvols, vol.mount_options)
|
||||||
|
|
||||||
def _lvm_create_pvs(
|
def _lvm_create_pvs(
|
||||||
|
|
@ -254,7 +243,7 @@ class FilesystemHandler:
|
||||||
for vg in lvm_config.vol_groups:
|
for vg in lvm_config.vol_groups:
|
||||||
pv_paths |= self._get_all_pv_dev_paths(vg.pvs, enc_mods)
|
pv_paths |= self._get_all_pv_dev_paths(vg.pvs, enc_mods)
|
||||||
|
|
||||||
device_handler.lvm_pv_create(pv_paths)
|
lvm_pv_create(pv_paths)
|
||||||
|
|
||||||
def _get_all_pv_dev_paths(
|
def _get_all_pv_dev_paths(
|
||||||
self,
|
self,
|
||||||
|
|
@ -329,27 +318,10 @@ class FilesystemHandler:
|
||||||
# from arch wiki:
|
# from arch wiki:
|
||||||
# If a logical volume will be formatted with ext4, leave at least 256 MiB
|
# If a logical volume will be formatted with ext4, leave at least 256 MiB
|
||||||
# free space in the volume group to allow using e2scrub
|
# free space in the volume group to allow using e2scrub
|
||||||
if any([vol.fs_type == FilesystemType.Ext4 for vol in vol_gp.volumes]):
|
if any([vol.fs_type == FilesystemType.EXT4 for vol in vol_gp.volumes]):
|
||||||
largest_vol = max(vol_gp.volumes, key=lambda x: x.length)
|
largest_vol = max(vol_gp.volumes, key=lambda x: x.length)
|
||||||
|
|
||||||
device_handler.lvm_vol_reduce(
|
lvm_vol_reduce(
|
||||||
largest_vol.safe_dev_path,
|
largest_vol.safe_dev_path,
|
||||||
Size(256, Unit.MiB, SectorSize.default()),
|
Size(256, Unit.MiB, SectorSize.default()),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _final_warning(self, device_paths: str) -> bool:
|
|
||||||
# Issue a final warning before we continue with something un-revertable.
|
|
||||||
# We mention the drive one last time, and count from 5 to 0.
|
|
||||||
out = tr(' ! Formatting {} in ').format(device_paths)
|
|
||||||
Tui.print(out, row=0, endl='', clear_screen=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
countdown = '\n5...4...3...2...1\n'
|
|
||||||
for c in countdown:
|
|
||||||
Tui.print(c, row=0, endl='')
|
|
||||||
time.sleep(0.25)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
with Tui():
|
|
||||||
ask_abort()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import shlex
|
import shlex
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import CalledProcessError
|
from subprocess import CalledProcessError
|
||||||
from types import TracebackType
|
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.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.device import DEFAULT_ITER_TIME
|
||||||
|
from archinstall.lib.models.users import Password
|
||||||
from .exceptions import DiskError, SysCallError
|
from archinstall.lib.utils.util import generate_password
|
||||||
from .general import SysCommand, SysCommandWorker, generate_password, run
|
|
||||||
from .models.users import Password
|
|
||||||
from .output import debug, info
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -42,10 +40,6 @@ class Luks2:
|
||||||
worker.poll()
|
worker.poll()
|
||||||
worker.write(b'YES\n', line_ending=False)
|
worker.write(b'YES\n', line_ending=False)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
if self.luks_dev_path is None:
|
|
||||||
raise ValueError('Partition must have a path set')
|
|
||||||
|
|
||||||
def __enter__(self) -> None:
|
def __enter__(self) -> None:
|
||||||
self.unlock(self.key_file)
|
self.unlock(self.key_file)
|
||||||
|
|
||||||
|
|
@ -210,6 +204,18 @@ class Luks2:
|
||||||
self._add_key(key_file)
|
self._add_key(key_file)
|
||||||
self._crypttab(crypttab_path, kf_path, options=['luks', 'key-slot=1'])
|
self._crypttab(crypttab_path, kf_path, options=['luks', 'key-slot=1'])
|
||||||
|
|
||||||
|
def create_crypttab_entry(self, target_path: Path) -> None:
|
||||||
|
"""
|
||||||
|
Add a crypttab entry without a keyfile so systemd prompts
|
||||||
|
for the passphrase at boot.
|
||||||
|
"""
|
||||||
|
if self.mapper_name is None:
|
||||||
|
raise ValueError('Mapper name must be provided')
|
||||||
|
|
||||||
|
crypttab_path = target_path / 'etc/crypttab'
|
||||||
|
crypttab_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._crypttab(crypttab_path, Path('none'), options=['luks'])
|
||||||
|
|
||||||
def _add_key(self, key_file: Path) -> None:
|
def _add_key(self, key_file: Path) -> None:
|
||||||
debug(f'Adding additional key-file {key_file}')
|
debug(f'Adding additional key-file {key_file}')
|
||||||
|
|
||||||
|
|
@ -238,3 +244,16 @@ class Luks2:
|
||||||
uuid = self._get_luks_uuid()
|
uuid = self._get_luks_uuid()
|
||||||
row = f'{self.mapper_name} UUID={uuid} {key_file} {opt}\n'
|
row = f'{self.mapper_name} UUID={uuid} {key_file} {opt}\n'
|
||||||
crypttab.write(row)
|
crypttab.write(row)
|
||||||
|
|
||||||
|
|
||||||
|
def unlock_luks2_dev(
|
||||||
|
dev_path: Path,
|
||||||
|
mapper_name: str,
|
||||||
|
enc_password: Password | None,
|
||||||
|
) -> Luks2:
|
||||||
|
luks_handler = Luks2(dev_path, mapper_name=mapper_name, password=enc_password)
|
||||||
|
|
||||||
|
if not luks_handler.is_unlocked():
|
||||||
|
luks_handler.unlock()
|
||||||
|
|
||||||
|
return luks_handler
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from pathlib import Path
|
||||||
|
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,
|
||||||
|
LvmVolume,
|
||||||
|
LvmVolumeGroup,
|
||||||
|
LvmVolumeInfo,
|
||||||
|
SectorSize,
|
||||||
|
Size,
|
||||||
|
Unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _lvm_info(
|
||||||
|
cmd: str,
|
||||||
|
info_type: Literal['lv', 'vg', 'pvseg'],
|
||||||
|
) -> LvmVolumeInfo | LvmGroupInfo | LvmPVInfo | None:
|
||||||
|
raw_info = SysCommand(cmd).decode().split('\n')
|
||||||
|
|
||||||
|
# for whatever reason the output sometimes contains
|
||||||
|
# "File descriptor X leaked leaked on vgs invocation
|
||||||
|
data = '\n'.join(raw for raw in raw_info if 'File descriptor' not in raw)
|
||||||
|
|
||||||
|
debug(f'LVM info: {data}')
|
||||||
|
|
||||||
|
reports = json.loads(data)
|
||||||
|
|
||||||
|
for report in reports['report']:
|
||||||
|
if len(report[info_type]) != 1:
|
||||||
|
raise ValueError('Report does not contain any entry')
|
||||||
|
|
||||||
|
entry = report[info_type][0]
|
||||||
|
|
||||||
|
match info_type:
|
||||||
|
case 'pvseg':
|
||||||
|
return LvmPVInfo(
|
||||||
|
pv_name=Path(entry['pv_name']),
|
||||||
|
lv_name=entry['lv_name'],
|
||||||
|
vg_name=entry['vg_name'],
|
||||||
|
)
|
||||||
|
case 'lv':
|
||||||
|
return LvmVolumeInfo(
|
||||||
|
lv_name=entry['lv_name'],
|
||||||
|
vg_name=entry['vg_name'],
|
||||||
|
lv_size=Size(int(entry['lv_size'][:-1]), Unit.B, SectorSize.default()),
|
||||||
|
)
|
||||||
|
case 'vg':
|
||||||
|
return LvmGroupInfo(
|
||||||
|
vg_uuid=entry['vg_uuid'],
|
||||||
|
vg_size=Size(int(entry['vg_size'][:-1]), Unit.B, SectorSize.default()),
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def _lvm_info_with_retry(cmd: str, info_type: Literal['lv']) -> LvmVolumeInfo | None: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def _lvm_info_with_retry(cmd: str, info_type: Literal['vg']) -> LvmGroupInfo | None: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def _lvm_info_with_retry(cmd: str, info_type: Literal['pvseg']) -> LvmPVInfo | None: ...
|
||||||
|
|
||||||
|
|
||||||
|
def _lvm_info_with_retry(
|
||||||
|
cmd: str,
|
||||||
|
info_type: Literal['lv', 'vg', 'pvseg'],
|
||||||
|
) -> LvmVolumeInfo | LvmGroupInfo | LvmPVInfo | None:
|
||||||
|
# Retry for up to 5 mins
|
||||||
|
max_retries = 100
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
return _lvm_info(cmd, info_type)
|
||||||
|
except ValueError:
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
debug(f'LVM info query failed (attempt {attempt + 1}/{max_retries}), retrying in 3 seconds...')
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
debug(f'LVM info query failed after {max_retries} attempts')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def lvm_vol_info(lv_name: str) -> LvmVolumeInfo | None:
|
||||||
|
cmd = f'lvs --reportformat json --unit B -S lv_name={lv_name}'
|
||||||
|
|
||||||
|
return _lvm_info_with_retry(cmd, 'lv')
|
||||||
|
|
||||||
|
|
||||||
|
def lvm_group_info(vg_name: str) -> LvmGroupInfo | None:
|
||||||
|
cmd = f'vgs --reportformat json --unit B -o vg_name,vg_uuid,vg_size -S vg_name={vg_name}'
|
||||||
|
|
||||||
|
return _lvm_info_with_retry(cmd, 'vg')
|
||||||
|
|
||||||
|
|
||||||
|
def lvm_pvseg_info(vg_name: str, lv_name: str) -> LvmPVInfo | None:
|
||||||
|
cmd = f'pvs --segments -o+lv_name,vg_name -S vg_name={vg_name},lv_name={lv_name} --reportformat json '
|
||||||
|
|
||||||
|
return _lvm_info_with_retry(cmd, 'pvseg')
|
||||||
|
|
||||||
|
|
||||||
|
def lvm_vol_change(vol: LvmVolume, activate: bool) -> None:
|
||||||
|
active_flag = 'y' if activate else 'n'
|
||||||
|
cmd = f'lvchange -a {active_flag} {vol.safe_dev_path}'
|
||||||
|
|
||||||
|
debug(f'lvchange volume: {cmd}')
|
||||||
|
SysCommand(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def lvm_export_vg(vg: LvmVolumeGroup) -> None:
|
||||||
|
cmd = f'vgexport {vg.name}'
|
||||||
|
|
||||||
|
debug(f'vgexport: {cmd}')
|
||||||
|
SysCommand(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def lvm_import_vg(vg: LvmVolumeGroup) -> None:
|
||||||
|
# Check if the VG is actually exported before trying to import it
|
||||||
|
check_cmd = f'vgs --noheadings -o vg_exported {vg.name}'
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = SysCommand(check_cmd)
|
||||||
|
is_exported = result.decode().strip() == 'exported'
|
||||||
|
except SysCallError:
|
||||||
|
# VG might not exist yet, skip import
|
||||||
|
debug(f'Volume group {vg.name} not found, skipping import')
|
||||||
|
return
|
||||||
|
|
||||||
|
if not is_exported:
|
||||||
|
debug(f'Volume group {vg.name} is already active (not exported), skipping import')
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = f'vgimport {vg.name}'
|
||||||
|
debug(f'vgimport: {cmd}')
|
||||||
|
SysCommand(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def lvm_vol_reduce(vol_path: Path, amount: Size) -> None:
|
||||||
|
val = amount.format_size(Unit.B, include_unit=False)
|
||||||
|
cmd = f'lvreduce -L -{val}B {vol_path}'
|
||||||
|
|
||||||
|
debug(f'Reducing LVM volume size: {cmd}')
|
||||||
|
SysCommand(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def lvm_pv_create(pvs: Iterable[Path]) -> None:
|
||||||
|
pvs_str = ' '.join(str(pv) for pv in pvs)
|
||||||
|
# Signatures are already wiped by wipefs, -f is just for safety
|
||||||
|
cmd = f'pvcreate -f --yes {pvs_str}'
|
||||||
|
# note flags used in scripting
|
||||||
|
debug(f'Creating LVM PVS: {cmd}')
|
||||||
|
SysCommand(cmd)
|
||||||
|
|
||||||
|
# Sync with udev to ensure the PVs are visible
|
||||||
|
udev_sync()
|
||||||
|
|
||||||
|
|
||||||
|
def lvm_vg_create(pvs: Iterable[Path], vg_name: str) -> None:
|
||||||
|
pvs_str = ' '.join(str(pv) for pv in pvs)
|
||||||
|
cmd = f'vgcreate --yes --force {vg_name} {pvs_str}'
|
||||||
|
|
||||||
|
debug(f'Creating LVM group: {cmd}')
|
||||||
|
SysCommand(cmd)
|
||||||
|
|
||||||
|
# Sync with udev to ensure the VG is visible
|
||||||
|
udev_sync()
|
||||||
|
|
||||||
|
|
||||||
|
def lvm_vol_create(vg_name: str, volume: LvmVolume, offset: Size | None = None) -> None:
|
||||||
|
if offset is not None:
|
||||||
|
length = volume.length - offset
|
||||||
|
else:
|
||||||
|
length = volume.length
|
||||||
|
|
||||||
|
length_str = length.format_size(Unit.B, include_unit=False)
|
||||||
|
cmd = f'lvcreate --yes -L {length_str}B {vg_name} -n {volume.name}'
|
||||||
|
|
||||||
|
debug(f'Creating volume: {cmd}')
|
||||||
|
|
||||||
|
worker = SysCommandWorker(cmd)
|
||||||
|
worker.poll()
|
||||||
|
worker.write(b'y\n', line_ending=False)
|
||||||
|
|
||||||
|
volume.vg_name = vg_name
|
||||||
|
volume.dev_path = Path(f'/dev/{vg_name}/{volume.name}')
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
|
from archinstall.lib.disk.subvolume_menu import SubvolumeMenu
|
||||||
|
from archinstall.lib.menu.helpers import Confirmation, Input, Selection
|
||||||
|
from archinstall.lib.menu.list_manager import ListManager
|
||||||
|
from archinstall.lib.menu.util import prompt_dir
|
||||||
from archinstall.lib.models.device import (
|
from archinstall.lib.models.device import (
|
||||||
BtrfsMountOption,
|
BtrfsMountOption,
|
||||||
DeviceModification,
|
DeviceModification,
|
||||||
|
|
@ -18,15 +20,9 @@ from archinstall.lib.models.device import (
|
||||||
Unit,
|
Unit,
|
||||||
)
|
)
|
||||||
from archinstall.lib.translationhandler import tr
|
from archinstall.lib.translationhandler import tr
|
||||||
from archinstall.tui.curses_menu import EditMenu, SelectMenu
|
from archinstall.lib.utils.format import as_table
|
||||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||||
from archinstall.tui.result import ResultType
|
from archinstall.tui.result import ResultType
|
||||||
from archinstall.tui.types import Alignment, FrameProperties, Orientation
|
|
||||||
|
|
||||||
from ..menu.list_manager import ListManager
|
|
||||||
from ..output import FormattedOutput
|
|
||||||
from ..utils.util import prompt_dir
|
|
||||||
from .subvolume_menu import SubvolumeMenu
|
|
||||||
|
|
||||||
|
|
||||||
class FreeSpace:
|
class FreeSpace:
|
||||||
|
|
@ -61,8 +57,8 @@ class DiskSegment:
|
||||||
return self.segment.table_data()
|
return self.segment.table_data()
|
||||||
|
|
||||||
part_mod = PartitionModification(
|
part_mod = PartitionModification(
|
||||||
status=ModificationStatus.Create,
|
status=ModificationStatus.CREATE,
|
||||||
type=PartitionType._Unknown,
|
type=PartitionType._UNKNOWN,
|
||||||
start=self.segment.start,
|
start=self.segment.start,
|
||||||
length=self.segment.length,
|
length=self.segment.length,
|
||||||
)
|
)
|
||||||
|
|
@ -193,23 +189,27 @@ class PartitioningList(ListManager[DiskSegment]):
|
||||||
def get_part_mods(disk_segments: list[DiskSegment]) -> list[PartitionModification]:
|
def get_part_mods(disk_segments: list[DiskSegment]) -> list[PartitionModification]:
|
||||||
return [s.segment for s in disk_segments if isinstance(s.segment, PartitionModification)]
|
return [s.segment for s in disk_segments if isinstance(s.segment, PartitionModification)]
|
||||||
|
|
||||||
def get_device_mod(self) -> DeviceModification:
|
async def show(self) -> DeviceModification | None:
|
||||||
disk_segments = super().run()
|
disk_segments = await super()._run()
|
||||||
|
|
||||||
|
if not disk_segments:
|
||||||
|
return None
|
||||||
|
|
||||||
partitions = self.get_part_mods(disk_segments)
|
partitions = self.get_part_mods(disk_segments)
|
||||||
return DeviceModification(self._device, self._wipe, partitions)
|
return DeviceModification(self._device, self._wipe, partitions)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def _run_actions_on_entry(self, entry: DiskSegment) -> None:
|
async def _run_actions_on_entry(self, entry: DiskSegment) -> None:
|
||||||
# Do not create a menu when the segment is free space
|
# Do not create a menu when the segment is free space
|
||||||
if isinstance(entry.segment, FreeSpace):
|
if isinstance(entry.segment, FreeSpace):
|
||||||
self._data = self.handle_action('', entry, self._data)
|
self._data = await self.handle_action('', entry, self._data)
|
||||||
else:
|
else:
|
||||||
super()._run_actions_on_entry(entry)
|
await super()._run_actions_on_entry(entry)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def selected_action_display(self, selection: DiskSegment) -> str:
|
def selected_action_display(self, selection: DiskSegment) -> str:
|
||||||
if isinstance(selection.segment, PartitionModification):
|
if isinstance(selection.segment, PartitionModification):
|
||||||
if selection.segment.status == ModificationStatus.Create:
|
if selection.segment.status == ModificationStatus.CREATE:
|
||||||
return tr('Partition - New')
|
return tr('Partition - New')
|
||||||
elif selection.segment.is_delete() and selection.segment.dev_path:
|
elif selection.segment.is_delete() and selection.segment.dev_path:
|
||||||
title = tr('Partition') + '\n\n'
|
title = tr('Partition') + '\n\n'
|
||||||
|
|
@ -238,7 +238,7 @@ class PartitioningList(ListManager[DiskSegment]):
|
||||||
# was marked as formatting, otherwise we run into issues where
|
# was marked as formatting, otherwise we run into issues where
|
||||||
# 1. select a new fs -> potentially mark as wipe now
|
# 1. select a new fs -> potentially mark as wipe now
|
||||||
# 2. Switch back to old filesystem -> should unmark wipe now, but
|
# 2. Switch back to old filesystem -> should unmark wipe now, but
|
||||||
# how do we know it was the original one?
|
# how do we know it was the original one?
|
||||||
not_filter += [
|
not_filter += [
|
||||||
self._actions['set_filesystem'],
|
self._actions['set_filesystem'],
|
||||||
self._actions['mark_bootable'],
|
self._actions['mark_bootable'],
|
||||||
|
|
@ -255,7 +255,7 @@ class PartitioningList(ListManager[DiskSegment]):
|
||||||
]
|
]
|
||||||
|
|
||||||
# non btrfs partitions shouldn't get btrfs options
|
# non btrfs partitions shouldn't get btrfs options
|
||||||
if selection.segment.fs_type != FilesystemType.Btrfs:
|
if selection.segment.fs_type != FilesystemType.BTRFS:
|
||||||
not_filter += [
|
not_filter += [
|
||||||
self._actions['btrfs_mark_compressed'],
|
self._actions['btrfs_mark_compressed'],
|
||||||
self._actions['btrfs_mark_nodatacow'],
|
self._actions['btrfs_mark_nodatacow'],
|
||||||
|
|
@ -267,7 +267,7 @@ class PartitioningList(ListManager[DiskSegment]):
|
||||||
return [o for o in options if o not in not_filter]
|
return [o for o in options if o not in not_filter]
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def handle_action(
|
async def handle_action(
|
||||||
self,
|
self,
|
||||||
action: str,
|
action: str,
|
||||||
entry: DiskSegment | None,
|
entry: DiskSegment | None,
|
||||||
|
|
@ -278,20 +278,20 @@ class PartitioningList(ListManager[DiskSegment]):
|
||||||
match action_key:
|
match action_key:
|
||||||
case 'suggest_partition_layout':
|
case 'suggest_partition_layout':
|
||||||
part_mods = self.get_part_mods(data)
|
part_mods = self.get_part_mods(data)
|
||||||
device_mod = self._suggest_partition_layout(part_mods)
|
device_mod = await self._suggest_partition_layout(part_mods)
|
||||||
if device_mod and device_mod.partitions:
|
if device_mod and device_mod.partitions:
|
||||||
data = self.as_segments(device_mod.partitions)
|
data = self.as_segments(device_mod.partitions)
|
||||||
self._wipe = device_mod.wipe
|
self._wipe = device_mod.wipe
|
||||||
self._prompt = self._info + self.wipe_str()
|
self._prompt = self._info + self.wipe_str()
|
||||||
case 'remove_added_partitions':
|
case 'remove_added_partitions':
|
||||||
if self._reset_confirmation():
|
if await self._reset_confirmation():
|
||||||
data = [s for s in data if isinstance(s.segment, PartitionModification) and s.segment.is_exists_or_modify()]
|
data = [s for s in data if isinstance(s.segment, PartitionModification) and s.segment.is_exists_or_modify()]
|
||||||
elif isinstance(entry.segment, PartitionModification):
|
elif isinstance(entry.segment, PartitionModification):
|
||||||
partition = entry.segment
|
partition = entry.segment
|
||||||
action_key = [k for k, v in self._actions.items() if v == action][0]
|
action_key = [k for k, v in self._actions.items() if v == action][0]
|
||||||
match action_key:
|
match action_key:
|
||||||
case 'assign_mountpoint':
|
case 'assign_mountpoint':
|
||||||
new_mountpoint = self._prompt_mountpoint()
|
new_mountpoint = await self._prompt_mountpoint()
|
||||||
if not partition.is_swap():
|
if not partition.is_swap():
|
||||||
if partition.is_home():
|
if partition.is_home():
|
||||||
partition.invert_flag(PartitionFlag.LINUX_HOME)
|
partition.invert_flag(PartitionFlag.LINUX_HOME)
|
||||||
|
|
@ -307,7 +307,7 @@ class PartitioningList(ListManager[DiskSegment]):
|
||||||
partition.flags = []
|
partition.flags = []
|
||||||
partition.set_flag(PartitionFlag.LINUX_HOME)
|
partition.set_flag(PartitionFlag.LINUX_HOME)
|
||||||
case 'mark_formatting':
|
case 'mark_formatting':
|
||||||
self._prompt_formatting(partition)
|
await self._prompt_formatting(partition)
|
||||||
case 'mark_bootable':
|
case 'mark_bootable':
|
||||||
if not partition.is_swap():
|
if not partition.is_swap():
|
||||||
partition.invert_flag(PartitionFlag.BOOT)
|
partition.invert_flag(PartitionFlag.BOOT)
|
||||||
|
|
@ -322,7 +322,7 @@ class PartitioningList(ListManager[DiskSegment]):
|
||||||
partition.invert_flag(PartitionFlag.ESP)
|
partition.invert_flag(PartitionFlag.ESP)
|
||||||
partition.invert_flag(PartitionFlag.XBOOTLDR)
|
partition.invert_flag(PartitionFlag.XBOOTLDR)
|
||||||
case 'set_filesystem':
|
case 'set_filesystem':
|
||||||
fs_type = self._prompt_partition_fs_type()
|
fs_type = await self._prompt_partition_fs_type()
|
||||||
|
|
||||||
if partition.is_swap():
|
if partition.is_swap():
|
||||||
partition.invert_flag(PartitionFlag.SWAP)
|
partition.invert_flag(PartitionFlag.SWAP)
|
||||||
|
|
@ -332,20 +332,21 @@ class PartitioningList(ListManager[DiskSegment]):
|
||||||
partition.flags = []
|
partition.flags = []
|
||||||
partition.set_flag(PartitionFlag.SWAP)
|
partition.set_flag(PartitionFlag.SWAP)
|
||||||
# btrfs subvolumes will define mountpoints
|
# btrfs subvolumes will define mountpoints
|
||||||
if fs_type == FilesystemType.Btrfs:
|
if fs_type == FilesystemType.BTRFS:
|
||||||
partition.mountpoint = None
|
partition.mountpoint = None
|
||||||
case 'btrfs_mark_compressed':
|
case 'btrfs_mark_compressed':
|
||||||
self._toggle_mount_option(partition, BtrfsMountOption.compress)
|
self._toggle_mount_option(partition, BtrfsMountOption.compress)
|
||||||
case 'btrfs_mark_nodatacow':
|
case 'btrfs_mark_nodatacow':
|
||||||
self._toggle_mount_option(partition, BtrfsMountOption.nodatacow)
|
self._toggle_mount_option(partition, BtrfsMountOption.nodatacow)
|
||||||
case 'btrfs_set_subvolumes':
|
case 'btrfs_set_subvolumes':
|
||||||
self._set_btrfs_subvolumes(partition)
|
await self._set_btrfs_subvolumes(partition)
|
||||||
case 'delete_partition':
|
case 'delete_partition':
|
||||||
data = self._delete_partition(partition, data)
|
data = self._delete_partition(partition, data)
|
||||||
else:
|
else:
|
||||||
part_mods = self.get_part_mods(data)
|
part_mods = self.get_part_mods(data)
|
||||||
index = data.index(entry)
|
index = data.index(entry)
|
||||||
part_mods.insert(index, self._create_new_partition(entry.segment))
|
part = await self._create_new_partition(entry.segment)
|
||||||
|
part_mods.insert(index, part)
|
||||||
data = self.as_segments(part_mods)
|
data = self.as_segments(part_mods)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
@ -356,7 +357,7 @@ class PartitioningList(ListManager[DiskSegment]):
|
||||||
data: list[DiskSegment],
|
data: list[DiskSegment],
|
||||||
) -> list[DiskSegment]:
|
) -> list[DiskSegment]:
|
||||||
if entry.is_exists_or_modify():
|
if entry.is_exists_or_modify():
|
||||||
entry.status = ModificationStatus.Delete
|
entry.status = ModificationStatus.DELETE
|
||||||
part_mods = self.get_part_mods(data)
|
part_mods = self.get_part_mods(data)
|
||||||
else:
|
else:
|
||||||
part_mods = [d.segment for d in data if isinstance(d.segment, PartitionModification) and d.segment != entry]
|
part_mods = [d.segment for d in data if isinstance(d.segment, PartitionModification) and d.segment != entry]
|
||||||
|
|
@ -378,52 +379,53 @@ class PartitioningList(ListManager[DiskSegment]):
|
||||||
else:
|
else:
|
||||||
partition.mount_options = [o for o in partition.mount_options if o != option.value]
|
partition.mount_options = [o for o in partition.mount_options if o != option.value]
|
||||||
|
|
||||||
def _set_btrfs_subvolumes(self, partition: PartitionModification) -> None:
|
async def _set_btrfs_subvolumes(self, partition: PartitionModification) -> None:
|
||||||
partition.btrfs_subvols = SubvolumeMenu(
|
subvols = await SubvolumeMenu(
|
||||||
partition.btrfs_subvols,
|
partition.btrfs_subvols,
|
||||||
None,
|
None,
|
||||||
).run()
|
).show()
|
||||||
|
|
||||||
def _prompt_formatting(self, partition: PartitionModification) -> None:
|
if subvols is not None:
|
||||||
|
partition.btrfs_subvols = subvols
|
||||||
|
|
||||||
|
async def _prompt_formatting(self, partition: PartitionModification) -> None:
|
||||||
# an existing partition can toggle between Exist or Modify
|
# an existing partition can toggle between Exist or Modify
|
||||||
if partition.is_modify():
|
if partition.is_modify():
|
||||||
partition.status = ModificationStatus.Exist
|
partition.status = ModificationStatus.EXIST
|
||||||
return
|
return
|
||||||
elif partition.exists():
|
elif partition.exists():
|
||||||
partition.status = ModificationStatus.Modify
|
partition.status = ModificationStatus.MODIFY
|
||||||
|
|
||||||
# If we mark a partition for formatting, but the format is CRYPTO LUKS, there's no point in formatting it really
|
# If we mark a partition for formatting, but the format is CRYPTO LUKS, there's no point in formatting it really
|
||||||
# without asking the user which inner-filesystem they want to use. Since the flag 'encrypted' = True is already set,
|
# without asking the user which inner-filesystem they want to use. Since the flag 'encrypted' = True is already set,
|
||||||
# it's safe to change the filesystem for this partition.
|
# it's safe to change the filesystem for this partition.
|
||||||
if partition.fs_type == FilesystemType.Crypto_luks:
|
if partition.fs_type == FilesystemType.CRYPTO_LUKS:
|
||||||
prompt = tr('This partition is currently encrypted, to format it a filesystem has to be specified') + '\n'
|
prompt = tr('This partition is currently encrypted, to format it a filesystem has to be specified') + '\n'
|
||||||
fs_type = self._prompt_partition_fs_type(prompt)
|
fs_type = await self._prompt_partition_fs_type(prompt)
|
||||||
partition.fs_type = fs_type
|
partition.fs_type = fs_type
|
||||||
|
|
||||||
if fs_type == FilesystemType.Btrfs:
|
if fs_type == FilesystemType.BTRFS:
|
||||||
partition.mountpoint = None
|
partition.mountpoint = None
|
||||||
|
|
||||||
def _prompt_mountpoint(self) -> Path:
|
async def _prompt_mountpoint(self) -> Path:
|
||||||
header = tr('Partition mount-points are relative to inside the installation, the boot would be /boot as an example.') + '\n'
|
header = tr('Partition mount-points are relative to inside the installation, the boot would be /boot as an example.') + '\n\n'
|
||||||
prompt = tr('Mountpoint')
|
header += tr('Enter a mountpoint')
|
||||||
|
|
||||||
mountpoint = prompt_dir(prompt, header, validate=False, allow_skip=False)
|
mountpoint = await prompt_dir(header, validate=False, allow_skip=False)
|
||||||
assert mountpoint
|
assert mountpoint
|
||||||
|
|
||||||
return mountpoint
|
return mountpoint
|
||||||
|
|
||||||
def _prompt_partition_fs_type(self, prompt: str | None = None) -> FilesystemType:
|
async def _prompt_partition_fs_type(self, prompt: str | None = None) -> FilesystemType:
|
||||||
fs_types = filter(lambda fs: fs != FilesystemType.Crypto_luks, FilesystemType)
|
fs_types = filter(lambda fs: fs != FilesystemType.CRYPTO_LUKS, FilesystemType)
|
||||||
items = [MenuItem(fs.value, value=fs) for fs in fs_types]
|
items = [MenuItem(fs.value, value=fs) for fs in fs_types]
|
||||||
group = MenuItemGroup(items, sort_items=False)
|
group = MenuItemGroup(items, sort_items=False)
|
||||||
|
|
||||||
result = SelectMenu[FilesystemType](
|
result = await Selection[FilesystemType](
|
||||||
group,
|
group,
|
||||||
header=prompt,
|
header=prompt,
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
frame=FrameProperties.min(tr('Filesystem')),
|
|
||||||
allow_skip=False,
|
allow_skip=False,
|
||||||
).run()
|
).show()
|
||||||
|
|
||||||
match result.type_:
|
match result.type_:
|
||||||
case ResultType.Selection:
|
case ResultType.Selection:
|
||||||
|
|
@ -437,7 +439,7 @@ class PartitioningList(ListManager[DiskSegment]):
|
||||||
max_size: Size,
|
max_size: Size,
|
||||||
text: str,
|
text: str,
|
||||||
) -> Size | None:
|
) -> Size | None:
|
||||||
match = re.match(r'([0-9]+)([a-zA-Z|%]*)', text, re.I)
|
match = re.match(r'^\s*([0-9]+)\s*([a-zA-Z%]*)\s*$', text, re.I)
|
||||||
|
|
||||||
if not match:
|
if not match:
|
||||||
return None
|
return None
|
||||||
|
|
@ -463,7 +465,7 @@ class PartitioningList(ListManager[DiskSegment]):
|
||||||
|
|
||||||
return size
|
return size
|
||||||
|
|
||||||
def _prompt_size(self, free_space: FreeSpace) -> Size:
|
async def _prompt_size(self, free_space: FreeSpace) -> Size:
|
||||||
def validate(value: str | None) -> str | None:
|
def validate(value: str | None) -> str | None:
|
||||||
if not value:
|
if not value:
|
||||||
return None
|
return None
|
||||||
|
|
@ -477,7 +479,7 @@ class PartitioningList(ListManager[DiskSegment]):
|
||||||
sector_size = device_info.sector_size
|
sector_size = device_info.sector_size
|
||||||
|
|
||||||
text = tr('Selected free space segment on device {}:').format(device_info.path) + '\n\n'
|
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'
|
prompt = text + free_space_table + '\n'
|
||||||
|
|
||||||
max_sectors = free_space.length.format_size(Unit.sectors, sector_size)
|
max_sectors = free_space.length.format_size(Unit.sectors, sector_size)
|
||||||
|
|
@ -485,18 +487,16 @@ class PartitioningList(ListManager[DiskSegment]):
|
||||||
|
|
||||||
prompt += tr('Size: {} / {}').format(max_sectors, max_bytes) + '\n\n'
|
prompt += tr('Size: {} / {}').format(max_sectors, max_bytes) + '\n\n'
|
||||||
prompt += tr('All entered values can be suffixed with a unit: %, B, KB, KiB, MB, MiB...') + '\n'
|
prompt += tr('All entered values can be suffixed with a unit: %, B, KB, KiB, MB, MiB...') + '\n'
|
||||||
prompt += tr('If no unit is provided, the value is interpreted as sectors') + '\n'
|
prompt += tr('If no unit is provided, the value is interpreted as sectors') + '\n\n'
|
||||||
|
|
||||||
max_size = free_space.length
|
max_size = free_space.length
|
||||||
|
prompt += tr('Enter a size (default: {}): ').format(max_size.format_highest())
|
||||||
|
|
||||||
title = tr('Size (default: {}): ').format(max_size.format_highest())
|
result = await Input(
|
||||||
|
|
||||||
result = EditMenu(
|
|
||||||
title,
|
|
||||||
header=f'{prompt}\b',
|
header=f'{prompt}\b',
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
validator=validate,
|
validator_callback=validate,
|
||||||
).input()
|
).show()
|
||||||
|
|
||||||
size: Size | None = None
|
size: Size | None = None
|
||||||
|
|
||||||
|
|
@ -504,28 +504,30 @@ class PartitioningList(ListManager[DiskSegment]):
|
||||||
case ResultType.Skip:
|
case ResultType.Skip:
|
||||||
size = max_size
|
size = max_size
|
||||||
case ResultType.Selection:
|
case ResultType.Selection:
|
||||||
value = result.text()
|
value = result.get_value()
|
||||||
|
|
||||||
if value:
|
if value:
|
||||||
size = self._validate_value(sector_size, max_size, value)
|
size = self._validate_value(sector_size, max_size, value)
|
||||||
else:
|
else:
|
||||||
size = max_size
|
size = max_size
|
||||||
|
case _:
|
||||||
|
raise ValueError('Unhandled result type')
|
||||||
|
|
||||||
assert size
|
assert size
|
||||||
return size
|
return size
|
||||||
|
|
||||||
def _create_new_partition(self, free_space: FreeSpace) -> PartitionModification:
|
async def _create_new_partition(self, free_space: FreeSpace) -> PartitionModification:
|
||||||
length = self._prompt_size(free_space)
|
length = await self._prompt_size(free_space)
|
||||||
|
|
||||||
fs_type = self._prompt_partition_fs_type()
|
fs_type = await self._prompt_partition_fs_type()
|
||||||
|
|
||||||
mountpoint = None
|
mountpoint = None
|
||||||
if fs_type not in (FilesystemType.Btrfs, FilesystemType.LinuxSwap):
|
if fs_type not in (FilesystemType.BTRFS, FilesystemType.LINUX_SWAP):
|
||||||
mountpoint = self._prompt_mountpoint()
|
mountpoint = await self._prompt_mountpoint()
|
||||||
|
|
||||||
partition = PartitionModification(
|
partition = PartitionModification(
|
||||||
status=ModificationStatus.Create,
|
status=ModificationStatus.CREATE,
|
||||||
type=PartitionType.Primary,
|
type=PartitionType.PRIMARY,
|
||||||
start=free_space.start,
|
start=free_space.start,
|
||||||
length=length,
|
length=length,
|
||||||
fs_type=fs_type,
|
fs_type=fs_type,
|
||||||
|
|
@ -543,42 +545,41 @@ class PartitioningList(ListManager[DiskSegment]):
|
||||||
|
|
||||||
return partition
|
return partition
|
||||||
|
|
||||||
def _reset_confirmation(self) -> bool:
|
async def _reset_confirmation(self) -> bool:
|
||||||
prompt = tr('This will remove all newly added partitions, continue?') + '\n'
|
prompt = tr('This will remove all newly added partitions, continue?') + '\n'
|
||||||
|
|
||||||
result = SelectMenu[bool](
|
result = await Confirmation(
|
||||||
MenuItemGroup.yes_no(),
|
|
||||||
header=prompt,
|
header=prompt,
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
orientation=Orientation.HORIZONTAL,
|
|
||||||
columns=2,
|
|
||||||
reset_warning_msg=prompt,
|
|
||||||
allow_skip=False,
|
allow_skip=False,
|
||||||
).run()
|
allow_reset=False,
|
||||||
|
).show()
|
||||||
|
|
||||||
return result.item() == MenuItem.yes()
|
return result.item() == MenuItem.yes()
|
||||||
|
|
||||||
def _suggest_partition_layout(
|
async def _suggest_partition_layout(
|
||||||
self,
|
self,
|
||||||
data: list[PartitionModification],
|
data: list[PartitionModification],
|
||||||
) -> DeviceModification | None:
|
) -> DeviceModification | None:
|
||||||
# if modifications have been done already, inform the user
|
# if modifications have been done already, inform the user
|
||||||
# that this operation will erase those modifications
|
# that this operation will erase those modifications
|
||||||
if any([not entry.exists() for entry in data]):
|
if any([not entry.exists() for entry in data]):
|
||||||
if not self._reset_confirmation():
|
if not await self._reset_confirmation():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
from ..interactions.disk_conf import suggest_single_disk_layout
|
from archinstall.lib.disk.disk_menu import suggest_single_disk_layout
|
||||||
|
|
||||||
return suggest_single_disk_layout(self._device)
|
return await suggest_single_disk_layout(self._device)
|
||||||
|
|
||||||
|
|
||||||
def manual_partitioning(
|
async def manual_partitioning(
|
||||||
device_mod: DeviceModification,
|
device_mod: DeviceModification,
|
||||||
partition_table: PartitionTable,
|
partition_table: PartitionTable,
|
||||||
) -> DeviceModification | None:
|
) -> DeviceModification | None:
|
||||||
menu_list = PartitioningList(device_mod, partition_table)
|
menu_list = PartitioningList(device_mod, partition_table)
|
||||||
mod = menu_list.get_device_mod()
|
mod = await menu_list.show()
|
||||||
|
|
||||||
|
if not mod:
|
||||||
|
return None
|
||||||
|
|
||||||
if menu_list.is_last_choice_cancel():
|
if menu_list.is_last_choice_cancel():
|
||||||
return device_mod
|
return device_mod
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import assert_never, override
|
from typing import assert_never, override
|
||||||
|
|
||||||
|
from archinstall.lib.menu.helpers import Input
|
||||||
|
from archinstall.lib.menu.list_manager import ListManager
|
||||||
|
from archinstall.lib.menu.util import prompt_dir
|
||||||
from archinstall.lib.models.device import SubvolumeModification
|
from archinstall.lib.models.device import SubvolumeModification
|
||||||
from archinstall.lib.translationhandler import tr
|
from archinstall.lib.translationhandler import tr
|
||||||
from archinstall.tui.curses_menu import EditMenu
|
|
||||||
from archinstall.tui.result import ResultType
|
from archinstall.tui.result import ResultType
|
||||||
from archinstall.tui.types import Alignment
|
|
||||||
|
|
||||||
from ..menu.list_manager import ListManager
|
|
||||||
from ..utils.util import prompt_dir
|
|
||||||
|
|
||||||
|
|
||||||
class SubvolumeMenu(ListManager[SubvolumeModification]):
|
class SubvolumeMenu(ListManager[SubvolumeModification]):
|
||||||
|
|
@ -30,38 +28,40 @@ class SubvolumeMenu(ListManager[SubvolumeModification]):
|
||||||
prompt,
|
prompt,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def show(self) -> list[SubvolumeModification] | None:
|
||||||
|
return await super()._run()
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def selected_action_display(self, selection: SubvolumeModification) -> str:
|
def selected_action_display(self, selection: SubvolumeModification) -> str:
|
||||||
return str(selection.name)
|
return str(selection.name)
|
||||||
|
|
||||||
def _add_subvolume(self, preset: SubvolumeModification | None = None) -> SubvolumeModification | None:
|
async def _add_subvolume(self, preset: SubvolumeModification | None = None) -> SubvolumeModification | None:
|
||||||
def validate(value: str | None) -> str | None:
|
def validate(value: str | None) -> str | None:
|
||||||
if value:
|
if value:
|
||||||
return None
|
return None
|
||||||
return tr('Value cannot be empty')
|
return tr('Value cannot be empty')
|
||||||
|
|
||||||
result = EditMenu(
|
result = await Input(
|
||||||
tr('Subvolume name'),
|
header=tr('Enter subvolume name'),
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
default_text=str(preset.name) if preset else None,
|
default_value=str(preset.name) if preset else None,
|
||||||
validator=validate,
|
validator_callback=validate,
|
||||||
).input()
|
).show()
|
||||||
|
|
||||||
match result.type_:
|
match result.type_:
|
||||||
case ResultType.Skip:
|
case ResultType.Skip:
|
||||||
return preset
|
return preset
|
||||||
case ResultType.Selection:
|
case ResultType.Selection:
|
||||||
name = result.text()
|
name = result.get_value()
|
||||||
case ResultType.Reset:
|
case ResultType.Reset:
|
||||||
raise ValueError('Unhandled result type')
|
raise ValueError('Unhandled result type')
|
||||||
case _:
|
case _:
|
||||||
assert_never(result.type_)
|
assert_never(result.type_)
|
||||||
|
|
||||||
header = f'{tr("Subvolume name")}: {name}\n'
|
header = f'{tr("Subvolume name")}: {name}\n\n'
|
||||||
|
header += tr('Enter subvolume mountpoint')
|
||||||
|
|
||||||
path = prompt_dir(
|
path = await prompt_dir(
|
||||||
tr('Subvolume mountpoint'),
|
|
||||||
header=header,
|
header=header,
|
||||||
allow_skip=True,
|
allow_skip=True,
|
||||||
validate=True,
|
validate=True,
|
||||||
|
|
@ -74,29 +74,29 @@ class SubvolumeMenu(ListManager[SubvolumeModification]):
|
||||||
return SubvolumeModification(Path(name), path)
|
return SubvolumeModification(Path(name), path)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def handle_action(
|
async def handle_action(
|
||||||
self,
|
self,
|
||||||
action: str,
|
action: str,
|
||||||
entry: SubvolumeModification | None,
|
entry: SubvolumeModification | None,
|
||||||
data: list[SubvolumeModification],
|
data: list[SubvolumeModification],
|
||||||
) -> list[SubvolumeModification]:
|
) -> list[SubvolumeModification]:
|
||||||
if action == self._actions[0]: # add
|
if action == self._actions[0]:
|
||||||
new_subvolume = self._add_subvolume()
|
new_subvolume = await self._add_subvolume()
|
||||||
|
|
||||||
if new_subvolume is not None:
|
if new_subvolume is not None:
|
||||||
# in case a user with the same username as an existing user
|
# in case a user with the same username as an existing user
|
||||||
# was created we'll replace the existing one
|
# was created we'll replace the existing one
|
||||||
data = [d for d in data if d.name != new_subvolume.name]
|
data = [d for d in data if d.name != new_subvolume.name]
|
||||||
data += [new_subvolume]
|
data += [new_subvolume]
|
||||||
elif entry is not None: # edit
|
elif entry is not None:
|
||||||
if action == self._actions[1]: # edit subvolume
|
if action == self._actions[1]:
|
||||||
new_subvolume = self._add_subvolume(entry)
|
new_subvolume = await self._add_subvolume(entry)
|
||||||
|
|
||||||
if new_subvolume is not None:
|
if new_subvolume is not None:
|
||||||
# we'll remove the original subvolume and add the modified version
|
# we'll remove the original subvolume and add the modified version
|
||||||
data = [d for d in data if d.name != entry.name and d.name != new_subvolume.name]
|
data = [d for d in data if d.name != entry.name and d.name != new_subvolume.name]
|
||||||
data += [new_subvolume]
|
data += [new_subvolume]
|
||||||
elif action == self._actions[2]: # delete
|
elif action == self._actions[2]:
|
||||||
data = [d for d in data if d != entry]
|
data = [d for d in data if d != entry]
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ from pathlib import Path
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from archinstall.lib.command import SysCommand
|
||||||
from archinstall.lib.exceptions import DiskError, SysCallError
|
from archinstall.lib.exceptions import DiskError, SysCallError
|
||||||
from archinstall.lib.general import SysCommand
|
from archinstall.lib.log import debug, info, warn
|
||||||
from archinstall.lib.models.device import LsblkInfo
|
from archinstall.lib.models.device import LsblkInfo
|
||||||
from archinstall.lib.output import debug, warn
|
|
||||||
|
|
||||||
|
|
||||||
class LsblkOutput(BaseModel):
|
class LsblkOutput(BaseModel):
|
||||||
|
|
@ -67,12 +67,12 @@ def get_lsblk_output() -> LsblkOutput:
|
||||||
|
|
||||||
def find_lsblk_info(
|
def find_lsblk_info(
|
||||||
dev_path: Path | str,
|
dev_path: Path | str,
|
||||||
info: list[LsblkInfo],
|
info_list: list[LsblkInfo],
|
||||||
) -> LsblkInfo | None:
|
) -> LsblkInfo | None:
|
||||||
if isinstance(dev_path, str):
|
if isinstance(dev_path, str):
|
||||||
dev_path = Path(dev_path)
|
dev_path = Path(dev_path)
|
||||||
|
|
||||||
for lsblk_info in info:
|
for lsblk_info in info_list:
|
||||||
if lsblk_info.path == dev_path:
|
if lsblk_info.path == dev_path:
|
||||||
return lsblk_info
|
return lsblk_info
|
||||||
|
|
||||||
|
|
@ -110,6 +110,69 @@ def disk_layouts() -> str:
|
||||||
return lsblk_output.model_dump_json(indent=4)
|
return lsblk_output.model_dump_json(indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
def get_parent_device_path(dev_path: Path) -> Path:
|
||||||
|
lsblk = get_lsblk_info(dev_path)
|
||||||
|
return Path(f'/dev/{lsblk.pkname}')
|
||||||
|
|
||||||
|
|
||||||
|
def get_unique_path_for_device(dev_path: Path) -> Path | None:
|
||||||
|
paths = Path('/dev/disk/by-id').glob('*')
|
||||||
|
linked_targets = {p.resolve(): p for p in paths}
|
||||||
|
linked_wwn_targets = {p: linked_targets[p] for p in linked_targets if p.name.startswith('wwn-') or p.name.startswith('nvme-eui.')}
|
||||||
|
|
||||||
|
if dev_path in linked_wwn_targets:
|
||||||
|
return linked_wwn_targets[dev_path]
|
||||||
|
|
||||||
|
if dev_path in linked_targets:
|
||||||
|
return linked_targets[dev_path]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def udev_sync() -> None:
|
||||||
|
try:
|
||||||
|
SysCommand('udevadm settle')
|
||||||
|
except SysCallError as err:
|
||||||
|
debug(f'Failed to synchronize with udev: {err}')
|
||||||
|
|
||||||
|
|
||||||
|
def mount(
|
||||||
|
dev_path: Path,
|
||||||
|
target_mountpoint: Path,
|
||||||
|
mount_fs: str | None = None,
|
||||||
|
create_target_mountpoint: bool = True,
|
||||||
|
options: list[str] = [],
|
||||||
|
) -> None:
|
||||||
|
if create_target_mountpoint and not target_mountpoint.exists():
|
||||||
|
target_mountpoint.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if not target_mountpoint.exists():
|
||||||
|
raise ValueError('Target mountpoint does not exist')
|
||||||
|
|
||||||
|
lsblk_info = get_lsblk_info(dev_path)
|
||||||
|
if target_mountpoint in lsblk_info.mountpoints:
|
||||||
|
info(f'Device already mounted at {target_mountpoint}')
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = ['mount']
|
||||||
|
|
||||||
|
if len(options):
|
||||||
|
cmd.extend(('-o', ','.join(options)))
|
||||||
|
if mount_fs:
|
||||||
|
cmd.extend(('-t', mount_fs))
|
||||||
|
|
||||||
|
cmd.extend((str(dev_path), str(target_mountpoint)))
|
||||||
|
|
||||||
|
command = ' '.join(cmd)
|
||||||
|
|
||||||
|
debug(f'Mounting {dev_path}: {command}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
SysCommand(command)
|
||||||
|
except SysCallError as err:
|
||||||
|
raise DiskError(f'Could not mount {dev_path}: {command}\n{err.message}')
|
||||||
|
|
||||||
|
|
||||||
def umount(mountpoint: Path, recursive: bool = False) -> None:
|
def umount(mountpoint: Path, recursive: bool = False) -> None:
|
||||||
lsblk_info = get_lsblk_info(mountpoint)
|
lsblk_info = get_lsblk_info(mountpoint)
|
||||||
|
|
||||||
|
|
@ -126,3 +189,10 @@ def umount(mountpoint: Path, recursive: bool = False) -> None:
|
||||||
for path in lsblk_info.mountpoints:
|
for path in lsblk_info.mountpoints:
|
||||||
debug(f'Unmounting mountpoint: {path}')
|
debug(f'Unmounting mountpoint: {path}')
|
||||||
SysCommand(cmd + [str(path)])
|
SysCommand(cmd + [str(path)])
|
||||||
|
|
||||||
|
|
||||||
|
def swapon(path: Path) -> None:
|
||||||
|
try:
|
||||||
|
SysCommand(['swapon', str(path)])
|
||||||
|
except SysCallError as err:
|
||||||
|
raise DiskError(f'Could not enable swap {path}:\n{err.message}')
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from archinstall.lib.general.general_menu import (
|
||||||
|
select_archinstall_language,
|
||||||
|
select_hostname,
|
||||||
|
select_ntp,
|
||||||
|
select_timezone,
|
||||||
|
)
|
||||||
|
from archinstall.lib.general.system_menu import select_driver, select_kernel, select_swap
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'select_archinstall_language',
|
||||||
|
'select_driver',
|
||||||
|
'select_hostname',
|
||||||
|
'select_kernel',
|
||||||
|
'select_ntp',
|
||||||
|
'select_swap',
|
||||||
|
'select_timezone',
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
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.translationhandler import Language, tr
|
||||||
|
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||||
|
from archinstall.tui.result import ResultType
|
||||||
|
|
||||||
|
|
||||||
|
class PostInstallationAction(Enum):
|
||||||
|
EXIT = tr('Exit archinstall')
|
||||||
|
REBOOT = tr('Reboot system')
|
||||||
|
CHROOT = tr('chroot into installation for post-installation configurations')
|
||||||
|
|
||||||
|
|
||||||
|
async def select_ntp(preset: bool = True) -> bool:
|
||||||
|
header = tr('Would you like to use automatic time synchronization (NTP) with the default time servers?\n') + '\n'
|
||||||
|
header += (
|
||||||
|
tr(
|
||||||
|
'Hardware time and other post-configuration steps might be required in order for NTP to work.\nFor more information, please check the Arch wiki',
|
||||||
|
)
|
||||||
|
+ '\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await Confirmation(
|
||||||
|
header=header,
|
||||||
|
allow_skip=True,
|
||||||
|
preset=preset,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case ResultType.Selection:
|
||||||
|
return result.item() == MenuItem.yes()
|
||||||
|
case _:
|
||||||
|
raise ValueError('Unhandled return type')
|
||||||
|
|
||||||
|
|
||||||
|
async def select_hostname(preset: str | None = None) -> str | None:
|
||||||
|
result = await Input(
|
||||||
|
header=tr('Enter a hostname'),
|
||||||
|
allow_skip=True,
|
||||||
|
default_value=preset,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case ResultType.Selection:
|
||||||
|
hostname = result.get_value()
|
||||||
|
if len(hostname) < 1:
|
||||||
|
return None
|
||||||
|
return hostname
|
||||||
|
case ResultType.Reset:
|
||||||
|
raise ValueError('Unhandled result type')
|
||||||
|
|
||||||
|
|
||||||
|
async def select_timezone(preset: str | None = None) -> str | None:
|
||||||
|
default = 'UTC'
|
||||||
|
timezones = list_timezones()
|
||||||
|
|
||||||
|
items = [MenuItem(tz, value=tz) for tz in timezones]
|
||||||
|
group = MenuItemGroup(items, sort_items=True)
|
||||||
|
group.set_selected_by_value(preset)
|
||||||
|
group.set_default_by_value(default)
|
||||||
|
|
||||||
|
result = await Selection[str](
|
||||||
|
group,
|
||||||
|
header=tr('Select timezone'),
|
||||||
|
allow_reset=True,
|
||||||
|
allow_skip=True,
|
||||||
|
enable_filter=True,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case ResultType.Reset:
|
||||||
|
return default
|
||||||
|
case ResultType.Selection:
|
||||||
|
return result.get_value()
|
||||||
|
|
||||||
|
|
||||||
|
async def select_language(preset: str | None = None) -> str | None:
|
||||||
|
from archinstall.lib.locale.locale_menu import select_kb_layout
|
||||||
|
|
||||||
|
# We'll raise an exception in an upcoming version.
|
||||||
|
# from ..exceptions import Deprecated
|
||||||
|
# raise Deprecated("select_language() has been deprecated, use select_kb_layout() instead.")
|
||||||
|
|
||||||
|
# No need to translate this i feel, as it's a short lived message.
|
||||||
|
warn('select_language() is deprecated, use select_kb_layout() instead. select_language() will be removed in a future version')
|
||||||
|
|
||||||
|
return await select_kb_layout(preset)
|
||||||
|
|
||||||
|
|
||||||
|
async def select_archinstall_language(languages: list[Language], preset: Language) -> Language:
|
||||||
|
# these are the displayed language names which can either be
|
||||||
|
# the english name of a language or, if present, the
|
||||||
|
# name of the language in its own language
|
||||||
|
|
||||||
|
items = [MenuItem(lang.display_name, lang) for lang in languages]
|
||||||
|
group = MenuItemGroup(items, sort_items=True)
|
||||||
|
group.set_focus_by_value(preset)
|
||||||
|
|
||||||
|
title = 'NOTE: Console font will be set automatically for supported languages.\n'
|
||||||
|
title += 'For other languages, fonts can be found in "/usr/share/kbd/consolefonts"\n'
|
||||||
|
title += 'and set manually with: setfont <fontname>\n'
|
||||||
|
|
||||||
|
result = await Selection[Language](
|
||||||
|
header=title,
|
||||||
|
group=group,
|
||||||
|
allow_reset=False,
|
||||||
|
allow_skip=True,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case ResultType.Selection:
|
||||||
|
return result.get_value()
|
||||||
|
case ResultType.Reset:
|
||||||
|
raise ValueError('Language selection not handled')
|
||||||
|
|
||||||
|
|
||||||
|
async def select_post_installation(elapsed_time: float | None = None) -> PostInstallationAction:
|
||||||
|
header = 'Installation completed'
|
||||||
|
if elapsed_time is not None:
|
||||||
|
minutes = int(elapsed_time // 60)
|
||||||
|
seconds = int(elapsed_time % 60)
|
||||||
|
header += f' in {minutes}m{seconds}s' + '\n'
|
||||||
|
header += tr('What would you like to do next?') + '\n'
|
||||||
|
|
||||||
|
items = [MenuItem(action.value, value=action) for action in PostInstallationAction]
|
||||||
|
group = MenuItemGroup(items)
|
||||||
|
|
||||||
|
result = await Selection[PostInstallationAction](
|
||||||
|
group,
|
||||||
|
header=header,
|
||||||
|
allow_skip=False,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Selection:
|
||||||
|
return result.get_value()
|
||||||
|
case _:
|
||||||
|
raise ValueError('Post installation action not handled')
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
from typing import assert_never
|
||||||
|
|
||||||
|
from archinstall.lib.hardware import GfxDriver, SysInfo
|
||||||
|
from archinstall.lib.menu.helpers import Confirmation, Selection
|
||||||
|
from archinstall.lib.models.application import ZramAlgorithm, ZramConfiguration
|
||||||
|
from archinstall.lib.models.package_types import DEFAULT_KERNEL, Kernel
|
||||||
|
from archinstall.lib.translationhandler import tr
|
||||||
|
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||||
|
from archinstall.tui.result import ResultType
|
||||||
|
|
||||||
|
|
||||||
|
async def select_kernel(preset: list[Kernel] = []) -> list[Kernel]:
|
||||||
|
"""
|
||||||
|
Asks the user to select a kernel for system.
|
||||||
|
|
||||||
|
:return: The string as a selected kernel
|
||||||
|
:rtype: string
|
||||||
|
"""
|
||||||
|
group = MenuItemGroup.from_enum(Kernel, sort_items=True, preset=preset)
|
||||||
|
group.set_default_by_value(DEFAULT_KERNEL)
|
||||||
|
group.set_focus_by_value(DEFAULT_KERNEL)
|
||||||
|
|
||||||
|
result = await Selection[Kernel](
|
||||||
|
group,
|
||||||
|
header=tr('Select which kernel(s) to install'),
|
||||||
|
allow_skip=True,
|
||||||
|
allow_reset=True,
|
||||||
|
multi=True,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case ResultType.Reset:
|
||||||
|
return []
|
||||||
|
case ResultType.Selection:
|
||||||
|
return result.get_values()
|
||||||
|
|
||||||
|
|
||||||
|
async def select_uki(preset: bool = True) -> bool:
|
||||||
|
prompt = tr('Would you like to use unified kernel images?') + '\n'
|
||||||
|
|
||||||
|
result = await Confirmation(header=prompt, allow_skip=True, preset=preset).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case ResultType.Selection:
|
||||||
|
return result.get_value()
|
||||||
|
case ResultType.Reset:
|
||||||
|
raise ValueError('Unhandled result type')
|
||||||
|
|
||||||
|
|
||||||
|
async def select_driver(options: list[GfxDriver] = [], preset: GfxDriver | None = None) -> GfxDriver | None:
|
||||||
|
"""
|
||||||
|
Somewhat convoluted function, whose job is simple.
|
||||||
|
Select a graphics driver from a pre-defined set of popular options.
|
||||||
|
|
||||||
|
(The template xorg is for beginner users, not advanced, and should
|
||||||
|
there for appeal to the general public first and edge cases later)
|
||||||
|
"""
|
||||||
|
if not options:
|
||||||
|
options = [driver for driver in GfxDriver]
|
||||||
|
|
||||||
|
items = [
|
||||||
|
MenuItem(
|
||||||
|
o.value,
|
||||||
|
value=o,
|
||||||
|
preview_action=lambda x: x.value.packages_text() if x.value else None,
|
||||||
|
)
|
||||||
|
for o in options
|
||||||
|
]
|
||||||
|
|
||||||
|
group = MenuItemGroup(items, sort_items=True)
|
||||||
|
group.set_default_by_value(GfxDriver.AllOpenSource)
|
||||||
|
|
||||||
|
if preset is not None:
|
||||||
|
group.set_focus_by_value(preset)
|
||||||
|
|
||||||
|
header = ''
|
||||||
|
if SysInfo.has_amd_graphics():
|
||||||
|
header += tr('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.') + '\n'
|
||||||
|
if SysInfo.has_intel_graphics():
|
||||||
|
header += tr('For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n')
|
||||||
|
if SysInfo.has_nvidia_graphics():
|
||||||
|
header += tr('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n')
|
||||||
|
|
||||||
|
result = await Selection[GfxDriver](
|
||||||
|
group,
|
||||||
|
header=header,
|
||||||
|
allow_skip=True,
|
||||||
|
allow_reset=True,
|
||||||
|
preview_location='right',
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case ResultType.Reset:
|
||||||
|
return None
|
||||||
|
case ResultType.Selection:
|
||||||
|
return result.get_value()
|
||||||
|
|
||||||
|
|
||||||
|
async def select_swap(preset: ZramConfiguration = ZramConfiguration(enabled=True)) -> ZramConfiguration:
|
||||||
|
prompt = tr('Would you like to use swap on zram?') + '\n'
|
||||||
|
|
||||||
|
group = MenuItemGroup.yes_no()
|
||||||
|
group.set_default_by_value(True)
|
||||||
|
group.set_focus_by_value(preset.enabled)
|
||||||
|
|
||||||
|
result = await Confirmation(
|
||||||
|
header=prompt,
|
||||||
|
allow_skip=True,
|
||||||
|
preset=preset.enabled,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
return preset
|
||||||
|
case ResultType.Selection:
|
||||||
|
enabled = result.item() == MenuItem.yes()
|
||||||
|
if not enabled:
|
||||||
|
return ZramConfiguration(enabled=False)
|
||||||
|
|
||||||
|
# Ask for compression algorithm
|
||||||
|
algo_group = MenuItemGroup.from_enum(ZramAlgorithm, sort_items=False)
|
||||||
|
algo_group.set_default_by_value(ZramAlgorithm.ZSTD)
|
||||||
|
algo_group.set_focus_by_value(preset.algorithm)
|
||||||
|
|
||||||
|
algo_result = await Selection[ZramAlgorithm](
|
||||||
|
algo_group,
|
||||||
|
header=tr('Select zram compression algorithm:') + '\n',
|
||||||
|
allow_skip=True,
|
||||||
|
).show()
|
||||||
|
|
||||||
|
match algo_result.type_:
|
||||||
|
case ResultType.Skip:
|
||||||
|
algo = preset.algorithm
|
||||||
|
case ResultType.Selection:
|
||||||
|
algo = algo_result.get_value()
|
||||||
|
case ResultType.Reset:
|
||||||
|
raise ValueError('Unhandled result type')
|
||||||
|
case _:
|
||||||
|
assert_never(algo_result.type_)
|
||||||
|
|
||||||
|
return ZramConfiguration(enabled=True, algorithm=algo)
|
||||||
|
case ResultType.Reset:
|
||||||
|
raise ValueError('Unhandled result type')
|
||||||
|
|
@ -1,65 +1,76 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import override
|
from typing import override
|
||||||
|
|
||||||
|
from archinstall.default_profiles.profile import GreeterType
|
||||||
|
from archinstall.lib.applications.application_menu import ApplicationMenu
|
||||||
|
from archinstall.lib.args import ArchConfig
|
||||||
|
from archinstall.lib.authentication.authentication_menu import AuthenticationMenu
|
||||||
|
from archinstall.lib.bootloader.bootloader_menu import BootloaderMenu
|
||||||
|
from archinstall.lib.bootloader.utils import validate_bootloader_layout
|
||||||
|
from archinstall.lib.configuration import ConfigurationOutput, save_config
|
||||||
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
|
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
|
||||||
from archinstall.lib.models.application import ApplicationConfiguration
|
from archinstall.lib.general.general_menu import select_hostname, select_ntp, select_timezone
|
||||||
|
from archinstall.lib.general.system_menu import select_kernel, select_swap
|
||||||
|
from archinstall.lib.hardware import SysInfo
|
||||||
|
from archinstall.lib.locale.locale_menu import LocaleMenu
|
||||||
|
from archinstall.lib.menu.abstract_menu import AbstractMenu, SpecialMenuKey
|
||||||
|
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
|
||||||
|
from archinstall.lib.mirror.mirror_menu import MirrorMenu
|
||||||
|
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
|
||||||
from archinstall.lib.models.authentication import AuthenticationConfiguration
|
from archinstall.lib.models.authentication import AuthenticationConfiguration
|
||||||
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, EncryptionType, FilesystemType, PartitionModification
|
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
|
||||||
from archinstall.lib.packages import list_available_packages
|
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, PartitionModification
|
||||||
|
from archinstall.lib.models.locale import LocaleConfiguration
|
||||||
|
from archinstall.lib.models.mirrors import MirrorConfiguration
|
||||||
|
from archinstall.lib.models.network import NetworkConfiguration, NicType
|
||||||
|
from archinstall.lib.models.package_types import DEFAULT_KERNEL
|
||||||
|
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.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
|
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
||||||
|
|
||||||
from .applications.application_menu import ApplicationMenu
|
|
||||||
from .args import ArchConfig
|
|
||||||
from .authentication.authentication_menu import AuthenticationMenu
|
|
||||||
from .configuration import save_config
|
|
||||||
from .hardware import SysInfo
|
|
||||||
from .interactions.general_conf import (
|
|
||||||
add_number_of_parallel_downloads,
|
|
||||||
ask_additional_packages_to_install,
|
|
||||||
ask_for_a_timezone,
|
|
||||||
ask_hostname,
|
|
||||||
ask_ntp,
|
|
||||||
)
|
|
||||||
from .interactions.network_menu import ask_to_configure_network
|
|
||||||
from .interactions.system_conf import ask_for_bootloader, ask_for_swap, ask_for_uki, select_kernel
|
|
||||||
from .locale.locale_menu import LocaleMenu
|
|
||||||
from .menu.abstract_menu import CONFIG_KEY, AbstractMenu
|
|
||||||
from .mirrors import MirrorMenu
|
|
||||||
from .models.bootloader import Bootloader
|
|
||||||
from .models.locale import LocaleConfiguration
|
|
||||||
from .models.mirrors import MirrorConfiguration
|
|
||||||
from .models.network import NetworkConfiguration, NicType
|
|
||||||
from .models.packages import Repository
|
|
||||||
from .models.profile import ProfileConfiguration
|
|
||||||
from .output import FormattedOutput
|
|
||||||
from .pacman.config import PacmanConfig
|
|
||||||
from .translationhandler import Language, tr, translation_handler
|
|
||||||
|
|
||||||
|
|
||||||
class GlobalMenu(AbstractMenu[None]):
|
class GlobalMenu(AbstractMenu[None]):
|
||||||
def __init__(self, arch_config: ArchConfig) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
arch_config: ArchConfig,
|
||||||
|
mirror_list_handler: MirrorListHandler | None = None,
|
||||||
|
skip_boot: bool = False,
|
||||||
|
advanced: bool = False,
|
||||||
|
title: str | None = None,
|
||||||
|
) -> None:
|
||||||
self._arch_config = arch_config
|
self._arch_config = arch_config
|
||||||
menu_optioons = self._get_menu_options()
|
self._mirror_list_handler = mirror_list_handler
|
||||||
|
self._skip_boot = skip_boot
|
||||||
|
self._advanced = advanced
|
||||||
|
self._uefi = SysInfo.has_uefi()
|
||||||
|
menu_options = self._get_menu_options()
|
||||||
|
|
||||||
self._item_group = MenuItemGroup(
|
self._item_group = MenuItemGroup(
|
||||||
menu_optioons,
|
menu_options,
|
||||||
sort_items=False,
|
sort_items=False,
|
||||||
checkmarks=True,
|
checkmarks=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
super().__init__(self._item_group, config=arch_config)
|
super().__init__(self._item_group, config=arch_config, title=title)
|
||||||
|
|
||||||
def _get_menu_options(self) -> list[MenuItem]:
|
def _get_menu_options(self) -> list[MenuItem]:
|
||||||
menu_options = [
|
menu_options = [
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text=tr('Archinstall language'),
|
text=tr('Archinstall language'),
|
||||||
action=self._select_archinstall_language,
|
action=self._select_archinstall_language,
|
||||||
display_action=lambda x: x.display_name if x else '',
|
preview_action=self._prev_archinstall_language,
|
||||||
key='archinstall_language',
|
key='archinstall_language',
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text=tr('Locales'),
|
text=tr('Locales'),
|
||||||
|
value=LocaleConfiguration.default(),
|
||||||
action=self._locale_selection,
|
action=self._locale_selection,
|
||||||
preview_action=self._prev_locale,
|
preview_action=self._prev_locale,
|
||||||
key='locale_config',
|
key='locale_config',
|
||||||
|
|
@ -79,31 +90,30 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text=tr('Swap'),
|
text=tr('Swap'),
|
||||||
value=True,
|
value=ZramConfiguration(enabled=True),
|
||||||
action=ask_for_swap,
|
action=select_swap,
|
||||||
preview_action=self._prev_swap,
|
preview_action=self._prev_swap,
|
||||||
key='swap',
|
key='swap',
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text=tr('Bootloader'),
|
text=tr('Bootloader'),
|
||||||
value=Bootloader.get_default(),
|
value=BootloaderConfiguration.get_default(self._uefi, self._skip_boot),
|
||||||
action=self._select_bootloader,
|
action=self._select_bootloader_config,
|
||||||
preview_action=self._prev_bootloader,
|
preview_action=self._prev_bootloader_config,
|
||||||
mandatory=True,
|
key='bootloader_config',
|
||||||
key='bootloader',
|
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text=tr('Unified kernel images'),
|
text=tr('Kernels'),
|
||||||
value=False,
|
value=[DEFAULT_KERNEL],
|
||||||
enabled=SysInfo.has_uefi(),
|
action=select_kernel,
|
||||||
action=ask_for_uki,
|
preview_action=self._prev_kernel,
|
||||||
preview_action=self._prev_uki,
|
mandatory=True,
|
||||||
key='uki',
|
key='kernels',
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text=tr('Hostname'),
|
text=tr('Hostname'),
|
||||||
value='archlinux',
|
value='archlinux',
|
||||||
action=ask_hostname,
|
action=select_hostname,
|
||||||
preview_action=self._prev_hostname,
|
preview_action=self._prev_hostname,
|
||||||
key='hostname',
|
key='hostname',
|
||||||
),
|
),
|
||||||
|
|
@ -126,27 +136,19 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
preview_action=self._prev_applications,
|
preview_action=self._prev_applications,
|
||||||
key='app_config',
|
key='app_config',
|
||||||
),
|
),
|
||||||
MenuItem(
|
|
||||||
text=tr('Kernels'),
|
|
||||||
value=['linux'],
|
|
||||||
action=select_kernel,
|
|
||||||
preview_action=self._prev_kernel,
|
|
||||||
mandatory=True,
|
|
||||||
key='kernels',
|
|
||||||
),
|
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text=tr('Network configuration'),
|
text=tr('Network configuration'),
|
||||||
action=ask_to_configure_network,
|
action=select_network,
|
||||||
value={},
|
value={},
|
||||||
preview_action=self._prev_network_config,
|
preview_action=self._prev_network_config,
|
||||||
key='network_config',
|
key='network_config',
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text=tr('Parallel Downloads'),
|
text=tr('Pacman'),
|
||||||
action=add_number_of_parallel_downloads,
|
action=self._pacman_configuration,
|
||||||
value=0,
|
value=PacmanConfiguration.default(),
|
||||||
preview_action=self._prev_parallel_dw,
|
preview_action=self._prev_pacman_config,
|
||||||
key='parallel_downloads',
|
key='pacman_config',
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text=tr('Additional packages'),
|
text=tr('Additional packages'),
|
||||||
|
|
@ -157,48 +159,48 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text=tr('Timezone'),
|
text=tr('Timezone'),
|
||||||
action=ask_for_a_timezone,
|
action=select_timezone,
|
||||||
value='UTC',
|
value='UTC',
|
||||||
preview_action=self._prev_tz,
|
preview_action=self._prev_tz,
|
||||||
key='timezone',
|
key='timezone',
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text=tr('Automatic time sync (NTP)'),
|
text=tr('Automatic time sync (NTP)'),
|
||||||
action=ask_ntp,
|
action=select_ntp,
|
||||||
value=True,
|
value=True,
|
||||||
preview_action=self._prev_ntp,
|
preview_action=self._prev_ntp,
|
||||||
key='ntp',
|
key='ntp',
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text='',
|
text='',
|
||||||
|
read_only=True,
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text=tr('Save configuration'),
|
text=tr('Save configuration'),
|
||||||
action=lambda x: self._safe_config(),
|
action=lambda x: self._safe_config(),
|
||||||
key=f'{CONFIG_KEY}_save',
|
key=SpecialMenuKey.SAVE.value,
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text=tr('Install'),
|
text=tr('Install'),
|
||||||
preview_action=self._prev_install_invalid_config,
|
preview_action=self._prev_install_invalid_config,
|
||||||
key=f'{CONFIG_KEY}_install',
|
key=SpecialMenuKey.INSTALL.value,
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
text=tr('Abort'),
|
text=tr('Abort'),
|
||||||
action=lambda x: exit(1),
|
key=SpecialMenuKey.ABORT.value,
|
||||||
key=f'{CONFIG_KEY}_abort',
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
return menu_options
|
return menu_options
|
||||||
|
|
||||||
def _safe_config(self) -> None:
|
async def _safe_config(self) -> None:
|
||||||
# data: dict[str, Any] = {}
|
# data: dict[str, Any] = {}
|
||||||
# for item in self._item_group.items:
|
# for item in self._item_group.items:
|
||||||
# if item.key is not None:
|
# if item.key is not None:
|
||||||
# data[item.key] = item.value
|
# data[item.key] = item.value
|
||||||
|
|
||||||
self.sync_all_to_config()
|
self.sync_all_to_config()
|
||||||
save_config(self._arch_config)
|
await save_config(self._arch_config)
|
||||||
|
|
||||||
def _missing_configs(self) -> list[str]:
|
def _missing_configs(self) -> list[str]:
|
||||||
item: MenuItem = self._item_group.find_by_key('auth_config')
|
item: MenuItem = self._item_group.find_by_key('auth_config')
|
||||||
|
|
@ -208,18 +210,25 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
item = self._item_group.find_by_key(s)
|
item = self._item_group.find_by_key(s)
|
||||||
return item.has_value()
|
return item.has_value()
|
||||||
|
|
||||||
def has_superuser() -> bool:
|
|
||||||
if auth_config and auth_config.users:
|
|
||||||
return any([u.sudo for u in auth_config.users])
|
|
||||||
return False
|
|
||||||
|
|
||||||
missing = set()
|
missing = set()
|
||||||
|
|
||||||
if (auth_config is None or auth_config.root_enc_password is None) and not has_superuser():
|
if (auth_config is None or auth_config.root_enc_password is None) and not (auth_config and auth_config.has_superuser()):
|
||||||
missing.add(
|
missing.add(
|
||||||
tr('Either root-password or at least 1 user with sudo privileges must be specified'),
|
tr('Either root-password or at least 1 user with sudo privileges must be specified'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# These greeters only show users with UID >= 1000 and have no manual login by default
|
||||||
|
if not (auth_config and auth_config.has_regular_user()):
|
||||||
|
profile_item: MenuItem = self._item_group.find_by_key('profile_config')
|
||||||
|
profile_config: ProfileConfiguration | None = profile_item.value
|
||||||
|
|
||||||
|
if profile_config and profile_config.profile and profile_config.profile.is_desktop_profile():
|
||||||
|
problematic_greeters = {GreeterType.Sddm}
|
||||||
|
if any(p.default_greeter_type in problematic_greeters for p in profile_config.profile.current_selection):
|
||||||
|
missing.add(
|
||||||
|
tr('The selected desktop profile requires a regular user to log in via the greeter'),
|
||||||
|
)
|
||||||
|
|
||||||
for item in self._item_group.items:
|
for item in self._item_group.items:
|
||||||
if item.mandatory:
|
if item.mandatory:
|
||||||
assert item.key is not None
|
assert item.key is not None
|
||||||
|
|
@ -229,7 +238,7 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
return list(missing)
|
return list(missing)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def _is_config_valid(self) -> bool:
|
def is_config_valid(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks the validity of the current configuration.
|
Checks the validity of the current configuration.
|
||||||
"""
|
"""
|
||||||
|
|
@ -237,22 +246,29 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
return False
|
return False
|
||||||
return self._validate_bootloader() is None
|
return self._validate_bootloader() is None
|
||||||
|
|
||||||
def _select_archinstall_language(self, preset: Language) -> Language:
|
async def _select_archinstall_language(self, preset: Language) -> Language:
|
||||||
from .interactions.general_conf import select_archinstall_language
|
from archinstall.lib.general.general_menu import select_archinstall_language
|
||||||
|
|
||||||
language = select_archinstall_language(translation_handler.translated_languages, preset)
|
language = await select_archinstall_language(translation_handler.translated_languages, preset)
|
||||||
translation_handler.activate(language)
|
translation_handler.activate(language)
|
||||||
|
|
||||||
self._update_lang_text()
|
self._update_lang_text()
|
||||||
|
|
||||||
return language
|
return language
|
||||||
|
|
||||||
def _select_applications(self, preset: ApplicationConfiguration | None) -> ApplicationConfiguration | None:
|
def _prev_archinstall_language(self, item: MenuItem) -> str | None:
|
||||||
app_config = ApplicationMenu(preset).run()
|
if not item.value:
|
||||||
|
return None
|
||||||
|
|
||||||
|
lang: Language = item.value
|
||||||
|
return f'{tr("Language")}: {lang.display_name}'
|
||||||
|
|
||||||
|
async def _select_applications(self, preset: ApplicationConfiguration | None) -> ApplicationConfiguration | None:
|
||||||
|
app_config = await ApplicationMenu(preset).show()
|
||||||
return app_config
|
return app_config
|
||||||
|
|
||||||
def _select_authentication(self, preset: AuthenticationConfiguration | None) -> AuthenticationConfiguration | None:
|
async def _select_authentication(self, preset: AuthenticationConfiguration | None) -> AuthenticationConfiguration | None:
|
||||||
auth_config = AuthenticationMenu(preset).run()
|
auth_config = await AuthenticationMenu(preset).show()
|
||||||
return auth_config
|
return auth_config
|
||||||
|
|
||||||
def _update_lang_text(self) -> None:
|
def _update_lang_text(self) -> None:
|
||||||
|
|
@ -266,8 +282,10 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
if o.key is not None:
|
if o.key is not None:
|
||||||
self._item_group.find_by_key(o.key).text = o.text
|
self._item_group.find_by_key(o.key).text = o.text
|
||||||
|
|
||||||
def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration:
|
tui.translate_bindings()
|
||||||
locale_config = LocaleMenu(preset).run()
|
|
||||||
|
async def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration | None:
|
||||||
|
locale_config = await LocaleMenu(preset).show()
|
||||||
return locale_config
|
return locale_config
|
||||||
|
|
||||||
def _prev_locale(self, item: MenuItem) -> str | None:
|
def _prev_locale(self, item: MenuItem) -> str | None:
|
||||||
|
|
@ -281,7 +299,7 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
if item.value:
|
if item.value:
|
||||||
network_config: NetworkConfiguration = item.value
|
network_config: NetworkConfiguration = item.value
|
||||||
if network_config.type == NicType.MANUAL:
|
if network_config.type == NicType.MANUAL:
|
||||||
output = FormattedOutput.as_table(network_config.nics)
|
output = as_table(network_config.nics)
|
||||||
else:
|
else:
|
||||||
output = f'{tr("Network configuration")}:\n{network_config.type.display_msg()}'
|
output = f'{tr("Network configuration")}:\n{network_config.type.display_msg()}'
|
||||||
|
|
||||||
|
|
@ -303,7 +321,7 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
output += f'{tr("Root password")}: {auth_config.root_enc_password.hidden()}\n'
|
output += f'{tr("Root password")}: {auth_config.root_enc_password.hidden()}\n'
|
||||||
|
|
||||||
if auth_config.users:
|
if auth_config.users:
|
||||||
output += FormattedOutput.as_table(auth_config.users) + '\n'
|
output += as_table(auth_config.users) + '\n'
|
||||||
|
|
||||||
if auth_config.u2f_config:
|
if auth_config.u2f_config:
|
||||||
u2f_config = auth_config.u2f_config
|
u2f_config = auth_config.u2f_config
|
||||||
|
|
@ -332,6 +350,21 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
output += f'{tr("Audio")}: {audio_config.audio.value}'
|
output += f'{tr("Audio")}: {audio_config.audio.value}'
|
||||||
output += '\n'
|
output += '\n'
|
||||||
|
|
||||||
|
if app_config.print_service_config:
|
||||||
|
output += f'{tr("Print service")}: '
|
||||||
|
output += tr('Enabled') if app_config.print_service_config.enabled else tr('Disabled')
|
||||||
|
output += '\n'
|
||||||
|
|
||||||
|
if app_config.power_management_config:
|
||||||
|
power_management_config = app_config.power_management_config
|
||||||
|
output += f'{tr("Power management")}: {power_management_config.power_management.value}'
|
||||||
|
output += '\n'
|
||||||
|
|
||||||
|
if app_config.firewall_config:
|
||||||
|
firewall_config = app_config.firewall_config
|
||||||
|
output += f'{tr("Firewall")}: {firewall_config.firewall.value}'
|
||||||
|
output += '\n'
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
@ -361,7 +394,7 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
output += '{}: {}'.format(tr('LVM configuration type'), disk_layout_conf.lvm_config.config_type.display_msg()) + '\n'
|
output += '{}: {}'.format(tr('LVM configuration type'), disk_layout_conf.lvm_config.config_type.display_msg()) + '\n'
|
||||||
|
|
||||||
if disk_layout_conf.disk_encryption:
|
if disk_layout_conf.disk_encryption:
|
||||||
output += tr('Disk encryption') + ': ' + EncryptionType.type_to_text(disk_layout_conf.disk_encryption.encryption_type) + '\n'
|
output += tr('Disk encryption') + ': ' + disk_layout_conf.disk_encryption.encryption_type.type_to_text() + '\n'
|
||||||
|
|
||||||
if disk_layout_conf.btrfs_options:
|
if disk_layout_conf.btrfs_options:
|
||||||
btrfs_options = disk_layout_conf.btrfs_options
|
btrfs_options = disk_layout_conf.btrfs_options
|
||||||
|
|
@ -375,14 +408,9 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
def _prev_swap(self, item: MenuItem) -> str | None:
|
def _prev_swap(self, item: MenuItem) -> str | None:
|
||||||
if item.value is not None:
|
if item.value is not None:
|
||||||
output = f'{tr("Swap on zram")}: '
|
output = f'{tr("Swap on zram")}: '
|
||||||
output += tr('Enabled') if item.value else tr('Disabled')
|
output += tr('Enabled') if item.value.enabled else tr('Disabled')
|
||||||
return output
|
if item.value.enabled:
|
||||||
return None
|
output += f'\n{tr("Compression algorithm")}: {item.value.algorithm.value}'
|
||||||
|
|
||||||
def _prev_uki(self, item: MenuItem) -> str | None:
|
|
||||||
if item.value is not None:
|
|
||||||
output = f'{tr("Unified kernel images")}: '
|
|
||||||
output += tr('Enabled') if item.value else tr('Disabled')
|
|
||||||
return output
|
return output
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -391,10 +419,18 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
return f'{tr("Hostname")}: {item.value}'
|
return f'{tr("Hostname")}: {item.value}'
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _prev_parallel_dw(self, item: MenuItem) -> str | None:
|
async def _pacman_configuration(self, preset: PacmanConfiguration) -> PacmanConfiguration | None:
|
||||||
if item.value is not None:
|
return await PacmanMenu(preset, advanced=self._advanced).show()
|
||||||
return f'{tr("Parallel Downloads")}: {item.value}'
|
|
||||||
return None
|
def _prev_pacman_config(self, item: MenuItem) -> str | None:
|
||||||
|
if not item.value:
|
||||||
|
return None
|
||||||
|
config: PacmanConfiguration = item.value
|
||||||
|
output = ''
|
||||||
|
if self._advanced:
|
||||||
|
output += '{}: {}\n'.format(tr('Parallel Downloads'), config.parallel_downloads)
|
||||||
|
output += '{}: {}'.format(tr('Color'), config.color)
|
||||||
|
return output
|
||||||
|
|
||||||
def _prev_kernel(self, item: MenuItem) -> str | None:
|
def _prev_kernel(self, item: MenuItem) -> str | None:
|
||||||
if item.value:
|
if item.value:
|
||||||
|
|
@ -402,9 +438,10 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
return f'{tr("Kernel")}: {kernel}'
|
return f'{tr("Kernel")}: {kernel}'
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _prev_bootloader(self, item: MenuItem) -> str | None:
|
def _prev_bootloader_config(self, item: MenuItem) -> str | None:
|
||||||
if item.value is not None:
|
bootloader_config: BootloaderConfiguration | None = item.value
|
||||||
return f'{tr("Bootloader")}: {item.value.value}'
|
if bootloader_config:
|
||||||
|
return bootloader_config.preview(self._uefi)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _validate_bootloader(self) -> str | None:
|
def _validate_bootloader(self) -> str | None:
|
||||||
|
|
@ -414,16 +451,16 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
|
|
||||||
Returns [`None`] if the bootloader is valid, otherwise returns a
|
Returns [`None`] if the bootloader is valid, otherwise returns a
|
||||||
string with the error message.
|
string with the error message.
|
||||||
|
|
||||||
XXX: The caller is responsible for wrapping the string with the translation
|
|
||||||
shim if necessary.
|
|
||||||
"""
|
"""
|
||||||
bootloader: Bootloader | None = None
|
bootloader_config: BootloaderConfiguration | None = None
|
||||||
root_partition: PartitionModification | None = None
|
root_partition: PartitionModification | None = None
|
||||||
boot_partition: PartitionModification | None = None
|
boot_partition: PartitionModification | None = None
|
||||||
efi_partition: PartitionModification | None = None
|
efi_partition: PartitionModification | None = None
|
||||||
|
|
||||||
bootloader = self._item_group.find_by_key('bootloader').value
|
bootloader_config = self._item_group.find_by_key('bootloader_config').value
|
||||||
|
|
||||||
|
if not bootloader_config or bootloader_config.bootloader == Bootloader.NO_BOOTLOADER:
|
||||||
|
return None
|
||||||
|
|
||||||
if disk_config := self._item_group.find_by_key('disk_config').value:
|
if disk_config := self._item_group.find_by_key('disk_config').value:
|
||||||
for layout in disk_config.device_modifications:
|
for layout in disk_config.device_modifications:
|
||||||
|
|
@ -432,7 +469,7 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
for layout in disk_config.device_modifications:
|
for layout in disk_config.device_modifications:
|
||||||
if boot_partition := layout.get_boot_partition():
|
if boot_partition := layout.get_boot_partition():
|
||||||
break
|
break
|
||||||
if SysInfo.has_uefi():
|
if self._uefi:
|
||||||
for layout in disk_config.device_modifications:
|
for layout in disk_config.device_modifications:
|
||||||
if efi_partition := layout.get_efi_partition():
|
if efi_partition := layout.get_efi_partition():
|
||||||
break
|
break
|
||||||
|
|
@ -445,16 +482,15 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
if boot_partition is None:
|
if boot_partition is None:
|
||||||
return 'Boot partition not found'
|
return 'Boot partition not found'
|
||||||
|
|
||||||
if SysInfo.has_uefi():
|
if self._uefi:
|
||||||
if efi_partition is None:
|
if efi_partition is None:
|
||||||
return 'EFI system partition (ESP) not found'
|
return 'EFI system partition (ESP) not found'
|
||||||
|
|
||||||
if efi_partition.fs_type not in [FilesystemType.Fat12, FilesystemType.Fat16, FilesystemType.Fat32]:
|
if efi_partition.fs_type is None or not efi_partition.fs_type.is_fat():
|
||||||
return 'ESP must be formatted as a FAT filesystem'
|
return 'ESP must be formatted as a FAT filesystem'
|
||||||
|
|
||||||
if bootloader == Bootloader.Limine:
|
if failure := validate_bootloader_layout(bootloader_config, disk_config):
|
||||||
if boot_partition.fs_type not in [FilesystemType.Fat12, FilesystemType.Fat16, FilesystemType.Fat32]:
|
return failure.description
|
||||||
return 'Limine does not support booting with a non-FAT boot partition'
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -466,9 +502,13 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
return text[:-1] # remove last new line
|
return text[:-1] # remove last new line
|
||||||
|
|
||||||
if error := self._validate_bootloader():
|
if error := self._validate_bootloader():
|
||||||
return tr(f'Invalid configuration: {error}')
|
return tr('Invalid configuration: {}').format(error)
|
||||||
|
|
||||||
return None
|
self.sync_all_to_config()
|
||||||
|
summary = ConfigurationOutput(self._arch_config).as_summary()
|
||||||
|
if summary:
|
||||||
|
return f'{tr("Ready to install")}\n\n{summary}'
|
||||||
|
return tr('Ready to install')
|
||||||
|
|
||||||
def _prev_profile(self, item: MenuItem) -> str | None:
|
def _prev_profile(self, item: MenuItem) -> str | None:
|
||||||
profile_config: ProfileConfiguration | None = item.value
|
profile_config: ProfileConfiguration | None = item.value
|
||||||
|
|
@ -490,51 +530,51 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _select_disk_config(
|
async def _select_disk_config(
|
||||||
self,
|
self,
|
||||||
preset: DiskLayoutConfiguration | None = None,
|
preset: DiskLayoutConfiguration | None = None,
|
||||||
) -> DiskLayoutConfiguration | None:
|
) -> DiskLayoutConfiguration | None:
|
||||||
disk_config = DiskLayoutConfigurationMenu(preset).run()
|
disk_config = await DiskLayoutConfigurationMenu(preset).show()
|
||||||
|
|
||||||
return disk_config
|
return disk_config
|
||||||
|
|
||||||
def _select_bootloader(self, preset: Bootloader | None) -> Bootloader | None:
|
async def _select_bootloader_config(
|
||||||
bootloader = ask_for_bootloader(preset)
|
self,
|
||||||
|
preset: BootloaderConfiguration | None = None,
|
||||||
|
) -> BootloaderConfiguration | None:
|
||||||
|
if preset is None:
|
||||||
|
preset = BootloaderConfiguration.get_default(self._uefi, self._skip_boot)
|
||||||
|
|
||||||
if bootloader:
|
bootloader_config = await BootloaderMenu(preset, self._uefi, self._skip_boot).show()
|
||||||
uki = self._item_group.find_by_key('uki')
|
|
||||||
if not SysInfo.has_uefi() or not bootloader.has_uki_support():
|
|
||||||
uki.value = False
|
|
||||||
uki.enabled = False
|
|
||||||
else:
|
|
||||||
uki.enabled = True
|
|
||||||
|
|
||||||
return bootloader
|
return bootloader_config
|
||||||
|
|
||||||
def _select_profile(self, current_profile: ProfileConfiguration | None) -> ProfileConfiguration | None:
|
async def _select_profile(self, current_profile: ProfileConfiguration | None) -> ProfileConfiguration | None:
|
||||||
from .profile.profile_menu import ProfileMenu
|
from archinstall.lib.profile.profile_menu import ProfileMenu
|
||||||
|
|
||||||
profile_config = ProfileMenu(preset=current_profile).run()
|
profile_config = await ProfileMenu(preset=current_profile).show()
|
||||||
return profile_config
|
return profile_config
|
||||||
|
|
||||||
def _select_additional_packages(self, preset: list[str]) -> list[str]:
|
async def _select_additional_packages(self, preset: list[str]) -> list[str]:
|
||||||
config: MirrorConfiguration | None = self._item_group.find_by_key('mirror_config').value
|
config: MirrorConfiguration | None = self._item_group.find_by_key('mirror_config').value
|
||||||
|
|
||||||
repositories: set[Repository] = set()
|
repositories: set[Repository] = set()
|
||||||
if config:
|
if config:
|
||||||
repositories = set(config.optional_repositories)
|
repositories = set(config.optional_repositories)
|
||||||
|
|
||||||
packages = ask_additional_packages_to_install(
|
packages = await select_additional_packages(
|
||||||
preset,
|
preset,
|
||||||
repositories=repositories,
|
repositories=repositories,
|
||||||
)
|
)
|
||||||
|
|
||||||
return packages
|
return packages
|
||||||
|
|
||||||
def _mirror_configuration(self, preset: MirrorConfiguration | None = None) -> MirrorConfiguration:
|
async def _mirror_configuration(self, preset: MirrorConfiguration | None = None) -> MirrorConfiguration | None:
|
||||||
mirror_configuration = MirrorMenu(preset=preset).run()
|
if self._mirror_list_handler is None:
|
||||||
|
self._mirror_list_handler = MirrorListHandler()
|
||||||
|
|
||||||
if mirror_configuration.optional_repositories:
|
mirror_configuration = await MirrorMenu(self._mirror_list_handler, preset=preset).run()
|
||||||
|
|
||||||
|
if mirror_configuration and mirror_configuration.optional_repositories:
|
||||||
# reset the package list cache in case the repository selection has changed
|
# reset the package list cache in case the repository selection has changed
|
||||||
list_available_packages.cache_clear()
|
list_available_packages.cache_clear()
|
||||||
|
|
||||||
|
|
@ -567,12 +607,12 @@ class GlobalMenu(AbstractMenu[None]):
|
||||||
if mirror_config.optional_repositories:
|
if mirror_config.optional_repositories:
|
||||||
title = tr('Optional repositories')
|
title = tr('Optional repositories')
|
||||||
divider = '-' * len(title)
|
divider = '-' * len(title)
|
||||||
repos = ', '.join([r.value for r in mirror_config.optional_repositories])
|
repos = ', '.join(r.value for r in mirror_config.optional_repositories)
|
||||||
output += f'{title}\n{divider}\n{repos}\n\n'
|
output += f'{title}\n{divider}\n{repos}\n\n'
|
||||||
|
|
||||||
if mirror_config.custom_repositories:
|
if mirror_config.custom_repositories:
|
||||||
title = tr('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}'
|
output += f'{title}:\n\n{table}'
|
||||||
|
|
||||||
return output.strip()
|
return output.strip()
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@ import os
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
from .exceptions import SysCallError
|
from archinstall.lib.command import SysCommand
|
||||||
from .general import SysCommand
|
from archinstall.lib.exceptions import SysCallError
|
||||||
from .networking import enrich_iface_types, list_interfaces
|
from archinstall.lib.log import debug
|
||||||
from .output import debug
|
from archinstall.lib.networking import enrich_iface_types, list_interfaces
|
||||||
from .translationhandler import tr
|
from archinstall.lib.translationhandler import tr
|
||||||
|
|
||||||
|
|
||||||
class CpuVendor(Enum):
|
class CpuVendor(Enum):
|
||||||
|
|
@ -16,7 +17,7 @@ class CpuVendor(Enum):
|
||||||
_Unknown = 'unknown'
|
_Unknown = 'unknown'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_vendor(cls, name: str) -> 'CpuVendor':
|
def get_vendor(cls, name: str) -> Self:
|
||||||
if vendor := getattr(cls, name, None):
|
if vendor := getattr(cls, name, None):
|
||||||
return vendor
|
return vendor
|
||||||
else:
|
else:
|
||||||
|
|
@ -40,10 +41,9 @@ class GfxPackage(Enum):
|
||||||
Dkms = 'dkms'
|
Dkms = 'dkms'
|
||||||
IntelMediaDriver = 'intel-media-driver'
|
IntelMediaDriver = 'intel-media-driver'
|
||||||
LibvaIntelDriver = 'libva-intel-driver'
|
LibvaIntelDriver = 'libva-intel-driver'
|
||||||
LibvaMesaDriver = 'libva-mesa-driver'
|
|
||||||
LibvaNvidiaDriver = 'libva-nvidia-driver'
|
LibvaNvidiaDriver = 'libva-nvidia-driver'
|
||||||
Mesa = 'mesa'
|
Mesa = 'mesa'
|
||||||
NvidiaDkms = 'nvidia-dkms'
|
NvidiaOpen = 'nvidia-open'
|
||||||
NvidiaOpenDkms = 'nvidia-open-dkms'
|
NvidiaOpenDkms = 'nvidia-open-dkms'
|
||||||
VulkanIntel = 'vulkan-intel'
|
VulkanIntel = 'vulkan-intel'
|
||||||
VulkanRadeon = 'vulkan-radeon'
|
VulkanRadeon = 'vulkan-radeon'
|
||||||
|
|
@ -51,8 +51,6 @@ class GfxPackage(Enum):
|
||||||
Xf86VideoAmdgpu = 'xf86-video-amdgpu'
|
Xf86VideoAmdgpu = 'xf86-video-amdgpu'
|
||||||
Xf86VideoAti = 'xf86-video-ati'
|
Xf86VideoAti = 'xf86-video-ati'
|
||||||
Xf86VideoNouveau = 'xf86-video-nouveau'
|
Xf86VideoNouveau = 'xf86-video-nouveau'
|
||||||
XorgServer = 'xorg-server'
|
|
||||||
XorgXinit = 'xorg-xinit'
|
|
||||||
|
|
||||||
|
|
||||||
class GfxDriver(Enum):
|
class GfxDriver(Enum):
|
||||||
|
|
@ -61,12 +59,24 @@ class GfxDriver(Enum):
|
||||||
IntelOpenSource = 'Intel (open-source)'
|
IntelOpenSource = 'Intel (open-source)'
|
||||||
NvidiaOpenKernel = 'Nvidia (open kernel module for newer GPUs, Turing+)'
|
NvidiaOpenKernel = 'Nvidia (open kernel module for newer GPUs, Turing+)'
|
||||||
NvidiaOpenSource = 'Nvidia (open-source nouveau driver)'
|
NvidiaOpenSource = 'Nvidia (open-source nouveau driver)'
|
||||||
NvidiaProprietary = 'Nvidia (proprietary)'
|
|
||||||
VMOpenSource = 'VirtualBox (open-source)'
|
VMOpenSource = 'VirtualBox (open-source)'
|
||||||
|
|
||||||
def is_nvidia(self) -> bool:
|
def is_nvidia(self) -> bool:
|
||||||
match self:
|
match self:
|
||||||
case GfxDriver.NvidiaProprietary | GfxDriver.NvidiaOpenSource | GfxDriver.NvidiaOpenKernel:
|
case GfxDriver.NvidiaOpenSource | GfxDriver.NvidiaOpenKernel:
|
||||||
|
return True
|
||||||
|
case _:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_nvidia_proprietary(self) -> bool:
|
||||||
|
"""
|
||||||
|
True for Nvidia drivers that ship proprietary userspace components.
|
||||||
|
Currently only NvidiaOpenKernel (nvidia-open-dkms): open kernel module
|
||||||
|
paired with proprietary userspace. NvidiaOpenSource (nouveau) is fully
|
||||||
|
open and works with Sway, so it is excluded.
|
||||||
|
"""
|
||||||
|
match self:
|
||||||
|
case GfxDriver.NvidiaOpenKernel:
|
||||||
return True
|
return True
|
||||||
case _:
|
case _:
|
||||||
return False
|
return False
|
||||||
|
|
@ -76,12 +86,12 @@ class GfxDriver(Enum):
|
||||||
text = tr('Installed packages') + ':\n'
|
text = tr('Installed packages') + ':\n'
|
||||||
|
|
||||||
for p in sorted(pkg_names):
|
for p in sorted(pkg_names):
|
||||||
text += f'\t- {p}\n'
|
text += f' - {p}\n'
|
||||||
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def gfx_packages(self) -> list[GfxPackage]:
|
def gfx_packages(self) -> list[GfxPackage]:
|
||||||
packages = [GfxPackage.XorgServer, GfxPackage.XorgXinit]
|
packages: list[GfxPackage] = []
|
||||||
|
|
||||||
match self:
|
match self:
|
||||||
case GfxDriver.AllOpenSource:
|
case GfxDriver.AllOpenSource:
|
||||||
|
|
@ -90,7 +100,6 @@ class GfxDriver(Enum):
|
||||||
GfxPackage.Xf86VideoAmdgpu,
|
GfxPackage.Xf86VideoAmdgpu,
|
||||||
GfxPackage.Xf86VideoAti,
|
GfxPackage.Xf86VideoAti,
|
||||||
GfxPackage.Xf86VideoNouveau,
|
GfxPackage.Xf86VideoNouveau,
|
||||||
GfxPackage.LibvaMesaDriver,
|
|
||||||
GfxPackage.LibvaIntelDriver,
|
GfxPackage.LibvaIntelDriver,
|
||||||
GfxPackage.IntelMediaDriver,
|
GfxPackage.IntelMediaDriver,
|
||||||
GfxPackage.VulkanRadeon,
|
GfxPackage.VulkanRadeon,
|
||||||
|
|
@ -102,7 +111,6 @@ class GfxDriver(Enum):
|
||||||
GfxPackage.Mesa,
|
GfxPackage.Mesa,
|
||||||
GfxPackage.Xf86VideoAmdgpu,
|
GfxPackage.Xf86VideoAmdgpu,
|
||||||
GfxPackage.Xf86VideoAti,
|
GfxPackage.Xf86VideoAti,
|
||||||
GfxPackage.LibvaMesaDriver,
|
|
||||||
GfxPackage.VulkanRadeon,
|
GfxPackage.VulkanRadeon,
|
||||||
]
|
]
|
||||||
case GfxDriver.IntelOpenSource:
|
case GfxDriver.IntelOpenSource:
|
||||||
|
|
@ -122,15 +130,8 @@ class GfxDriver(Enum):
|
||||||
packages += [
|
packages += [
|
||||||
GfxPackage.Mesa,
|
GfxPackage.Mesa,
|
||||||
GfxPackage.Xf86VideoNouveau,
|
GfxPackage.Xf86VideoNouveau,
|
||||||
GfxPackage.LibvaMesaDriver,
|
|
||||||
GfxPackage.VulkanNouveau,
|
GfxPackage.VulkanNouveau,
|
||||||
]
|
]
|
||||||
case GfxDriver.NvidiaProprietary:
|
|
||||||
packages += [
|
|
||||||
GfxPackage.NvidiaDkms,
|
|
||||||
GfxPackage.Dkms,
|
|
||||||
GfxPackage.LibvaNvidiaDriver,
|
|
||||||
]
|
|
||||||
case GfxDriver.VMOpenSource:
|
case GfxDriver.VMOpenSource:
|
||||||
packages += [
|
packages += [
|
||||||
GfxPackage.Mesa,
|
GfxPackage.Mesa,
|
||||||
|
|
@ -143,6 +144,18 @@ class _SysInfo:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def has_battery(self) -> bool:
|
||||||
|
for type_path in Path('/sys/class/power_supply/').glob('*/type'):
|
||||||
|
try:
|
||||||
|
with open(type_path) as f:
|
||||||
|
if f.read().strip() == 'Battery':
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def cpu_info(self) -> dict[str, str]:
|
def cpu_info(self) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -193,11 +206,27 @@ class _SysInfo:
|
||||||
|
|
||||||
return modules
|
return modules
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def graphics_devices(self) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
Returns detected graphics devices (cached)
|
||||||
|
"""
|
||||||
|
cards: dict[str, str] = {}
|
||||||
|
for line in SysCommand('lspci'):
|
||||||
|
if b' VGA ' in line or b' 3D ' in line:
|
||||||
|
_, identifier = line.split(b': ', 1)
|
||||||
|
cards[identifier.strip().decode('UTF-8')] = str(line)
|
||||||
|
return cards
|
||||||
|
|
||||||
|
|
||||||
_sys_info = _SysInfo()
|
_sys_info = _SysInfo()
|
||||||
|
|
||||||
|
|
||||||
class SysInfo:
|
class SysInfo:
|
||||||
|
@staticmethod
|
||||||
|
def has_battery() -> bool:
|
||||||
|
return _sys_info.has_battery
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def has_wifi() -> bool:
|
def has_wifi() -> bool:
|
||||||
ifaces = list(list_interfaces().values())
|
ifaces = list(list_interfaces().values())
|
||||||
|
|
@ -209,24 +238,19 @@ class SysInfo:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _graphics_devices() -> dict[str, str]:
|
def _graphics_devices() -> dict[str, str]:
|
||||||
cards: dict[str, str] = {}
|
return _sys_info.graphics_devices
|
||||||
for line in SysCommand('lspci'):
|
|
||||||
if b' VGA ' in line or b' 3D ' in line:
|
|
||||||
_, identifier = line.split(b': ', 1)
|
|
||||||
cards[identifier.strip().decode('UTF-8')] = str(line)
|
|
||||||
return cards
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def has_nvidia_graphics() -> bool:
|
def has_nvidia_graphics() -> bool:
|
||||||
return any('nvidia' in x.lower() for x in SysInfo._graphics_devices())
|
return any('nvidia' in x.lower() for x in _sys_info.graphics_devices)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def has_amd_graphics() -> bool:
|
def has_amd_graphics() -> bool:
|
||||||
return any('amd' in x.lower() for x in SysInfo._graphics_devices())
|
return any('amd' in x.lower() for x in _sys_info.graphics_devices)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def has_intel_graphics() -> bool:
|
def has_intel_graphics() -> bool:
|
||||||
return any('intel' in x.lower() for x in SysInfo._graphics_devices())
|
return any('intel' in x.lower() for x in _sys_info.graphics_devices)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def cpu_vendor() -> CpuVendor | None:
|
def cpu_vendor() -> CpuVendor | None:
|
||||||
|
|
@ -239,14 +263,20 @@ class SysInfo:
|
||||||
return _sys_info.cpu_info.get('model name', None)
|
return _sys_info.cpu_info.get('model name', None)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sys_vendor() -> str:
|
def sys_vendor() -> str | None:
|
||||||
with open('/sys/devices/virtual/dmi/id/sys_vendor') as vendor:
|
try:
|
||||||
return vendor.read().strip()
|
with open('/sys/devices/virtual/dmi/id/sys_vendor') as vendor:
|
||||||
|
return vendor.read().strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def product_name() -> str:
|
def product_name() -> str | None:
|
||||||
with open('/sys/devices/virtual/dmi/id/product_name') as product:
|
try:
|
||||||
return product.read().strip()
|
with open('/sys/devices/virtual/dmi/id/product_name') as product:
|
||||||
|
return product.read().strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def mem_available() -> int:
|
def mem_available() -> int:
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,43 +0,0 @@
|
||||||
from .disk_conf import (
|
|
||||||
get_default_partition_layout,
|
|
||||||
select_devices,
|
|
||||||
select_disk_config,
|
|
||||||
select_main_filesystem_format,
|
|
||||||
suggest_multi_disk_layout,
|
|
||||||
suggest_single_disk_layout,
|
|
||||||
)
|
|
||||||
from .general_conf import (
|
|
||||||
add_number_of_parallel_downloads,
|
|
||||||
ask_additional_packages_to_install,
|
|
||||||
ask_for_a_timezone,
|
|
||||||
ask_hostname,
|
|
||||||
ask_ntp,
|
|
||||||
select_archinstall_language,
|
|
||||||
)
|
|
||||||
from .manage_users_conf import UserList, ask_for_additional_users
|
|
||||||
from .network_menu import ManualNetworkConfig, ask_to_configure_network
|
|
||||||
from .system_conf import ask_for_bootloader, ask_for_swap, ask_for_uki, select_driver, select_kernel
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'ManualNetworkConfig',
|
|
||||||
'UserList',
|
|
||||||
'add_number_of_parallel_downloads',
|
|
||||||
'ask_additional_packages_to_install',
|
|
||||||
'ask_for_a_timezone',
|
|
||||||
'ask_for_additional_users',
|
|
||||||
'ask_for_bootloader',
|
|
||||||
'ask_for_swap',
|
|
||||||
'ask_for_uki',
|
|
||||||
'ask_hostname',
|
|
||||||
'ask_ntp',
|
|
||||||
'ask_to_configure_network',
|
|
||||||
'get_default_partition_layout',
|
|
||||||
'select_archinstall_language',
|
|
||||||
'select_devices',
|
|
||||||
'select_disk_config',
|
|
||||||
'select_driver',
|
|
||||||
'select_kernel',
|
|
||||||
'select_main_filesystem_format',
|
|
||||||
'suggest_multi_disk_layout',
|
|
||||||
'suggest_single_disk_layout',
|
|
||||||
]
|
|
||||||
|
|
@ -1,633 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from archinstall.lib.args import arch_config_handler
|
|
||||||
from archinstall.lib.disk.device_handler import device_handler
|
|
||||||
from archinstall.lib.disk.partitioning_menu import manual_partitioning
|
|
||||||
from archinstall.lib.menu.menu_helper import MenuHelper
|
|
||||||
from archinstall.lib.models.device import (
|
|
||||||
BDevice,
|
|
||||||
BtrfsMountOption,
|
|
||||||
DeviceModification,
|
|
||||||
DiskLayoutConfiguration,
|
|
||||||
DiskLayoutType,
|
|
||||||
FilesystemType,
|
|
||||||
LvmConfiguration,
|
|
||||||
LvmLayoutType,
|
|
||||||
LvmVolume,
|
|
||||||
LvmVolumeGroup,
|
|
||||||
LvmVolumeStatus,
|
|
||||||
ModificationStatus,
|
|
||||||
PartitionFlag,
|
|
||||||
PartitionModification,
|
|
||||||
PartitionType,
|
|
||||||
SectorSize,
|
|
||||||
Size,
|
|
||||||
SubvolumeModification,
|
|
||||||
Unit,
|
|
||||||
_DeviceInfo,
|
|
||||||
)
|
|
||||||
from archinstall.lib.output import debug
|
|
||||||
from archinstall.lib.translationhandler import tr
|
|
||||||
from archinstall.tui.curses_menu import SelectMenu
|
|
||||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
|
||||||
from archinstall.tui.result import ResultType
|
|
||||||
from archinstall.tui.types import Alignment, FrameProperties, Orientation, PreviewStyle
|
|
||||||
|
|
||||||
from ..output import FormattedOutput
|
|
||||||
from ..utils.util import prompt_dir
|
|
||||||
|
|
||||||
|
|
||||||
def select_devices(preset: list[BDevice] | None = []) -> list[BDevice]:
|
|
||||||
def _preview_device_selection(item: MenuItem) -> str | None:
|
|
||||||
device = item.get_value()
|
|
||||||
dev = device_handler.get_device(device.path)
|
|
||||||
|
|
||||||
if dev and dev.partition_infos:
|
|
||||||
return FormattedOutput.as_table(dev.partition_infos)
|
|
||||||
return None
|
|
||||||
|
|
||||||
if preset is None:
|
|
||||||
preset = []
|
|
||||||
|
|
||||||
devices = device_handler.devices
|
|
||||||
options = [d.device_info for d in devices]
|
|
||||||
presets = [p.device_info for p in preset]
|
|
||||||
|
|
||||||
group = MenuHelper(options).create_menu_group()
|
|
||||||
group.set_selected_by_value(presets)
|
|
||||||
group.set_preview_for_all(_preview_device_selection)
|
|
||||||
|
|
||||||
result = SelectMenu[_DeviceInfo](
|
|
||||||
group,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
search_enabled=False,
|
|
||||||
multi=True,
|
|
||||||
preview_style=PreviewStyle.BOTTOM,
|
|
||||||
preview_size='auto',
|
|
||||||
preview_frame=FrameProperties.max('Partitions'),
|
|
||||||
allow_skip=True,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Reset:
|
|
||||||
return []
|
|
||||||
case ResultType.Skip:
|
|
||||||
return preset
|
|
||||||
case ResultType.Selection:
|
|
||||||
selected_device_info = result.get_values()
|
|
||||||
selected_devices = []
|
|
||||||
|
|
||||||
for device in devices:
|
|
||||||
if device.device_info in selected_device_info:
|
|
||||||
selected_devices.append(device)
|
|
||||||
|
|
||||||
return selected_devices
|
|
||||||
|
|
||||||
|
|
||||||
def get_default_partition_layout(
|
|
||||||
devices: list[BDevice],
|
|
||||||
filesystem_type: FilesystemType | None = None,
|
|
||||||
) -> list[DeviceModification]:
|
|
||||||
if len(devices) == 1:
|
|
||||||
device_modification = suggest_single_disk_layout(
|
|
||||||
devices[0],
|
|
||||||
filesystem_type=filesystem_type,
|
|
||||||
)
|
|
||||||
return [device_modification]
|
|
||||||
else:
|
|
||||||
return suggest_multi_disk_layout(
|
|
||||||
devices,
|
|
||||||
filesystem_type=filesystem_type,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _manual_partitioning(
|
|
||||||
preset: list[DeviceModification],
|
|
||||||
devices: list[BDevice],
|
|
||||||
) -> list[DeviceModification]:
|
|
||||||
modifications = []
|
|
||||||
for device in devices:
|
|
||||||
mod = next(filter(lambda x: x.device == device, preset), None)
|
|
||||||
if not mod:
|
|
||||||
mod = DeviceModification(device, wipe=False)
|
|
||||||
|
|
||||||
if device_mod := manual_partitioning(mod, device_handler.partition_table):
|
|
||||||
modifications.append(device_mod)
|
|
||||||
|
|
||||||
return modifications
|
|
||||||
|
|
||||||
|
|
||||||
def select_disk_config(preset: DiskLayoutConfiguration | None = None) -> DiskLayoutConfiguration | None:
|
|
||||||
default_layout = DiskLayoutType.Default.display_msg()
|
|
||||||
manual_mode = DiskLayoutType.Manual.display_msg()
|
|
||||||
pre_mount_mode = DiskLayoutType.Pre_mount.display_msg()
|
|
||||||
|
|
||||||
items = [
|
|
||||||
MenuItem(default_layout, value=default_layout),
|
|
||||||
MenuItem(manual_mode, value=manual_mode),
|
|
||||||
MenuItem(pre_mount_mode, value=pre_mount_mode),
|
|
||||||
]
|
|
||||||
group = MenuItemGroup(items, sort_items=False)
|
|
||||||
|
|
||||||
if preset:
|
|
||||||
group.set_selected_by_value(preset.config_type.display_msg())
|
|
||||||
|
|
||||||
result = SelectMenu[str](
|
|
||||||
group,
|
|
||||||
allow_skip=True,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
frame=FrameProperties.min(tr('Disk configuration type')),
|
|
||||||
allow_reset=True,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Skip:
|
|
||||||
return preset
|
|
||||||
case ResultType.Reset:
|
|
||||||
return None
|
|
||||||
case ResultType.Selection:
|
|
||||||
selection = result.get_value()
|
|
||||||
|
|
||||||
if selection == pre_mount_mode:
|
|
||||||
output = 'You will use whatever drive-setup is mounted at the specified directory\n'
|
|
||||||
output += "WARNING: Archinstall won't check the suitability of this setup\n"
|
|
||||||
|
|
||||||
path = prompt_dir(tr('Root mount directory'), output, allow_skip=True)
|
|
||||||
|
|
||||||
if path is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
mods = device_handler.detect_pre_mounted_mods(path)
|
|
||||||
|
|
||||||
return DiskLayoutConfiguration(
|
|
||||||
config_type=DiskLayoutType.Pre_mount,
|
|
||||||
device_modifications=mods,
|
|
||||||
mountpoint=path,
|
|
||||||
)
|
|
||||||
|
|
||||||
preset_devices = [mod.device for mod in preset.device_modifications] if preset else []
|
|
||||||
devices = select_devices(preset_devices)
|
|
||||||
|
|
||||||
if not devices:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if result.get_value() == default_layout:
|
|
||||||
modifications = get_default_partition_layout(devices)
|
|
||||||
if modifications:
|
|
||||||
return DiskLayoutConfiguration(
|
|
||||||
config_type=DiskLayoutType.Default,
|
|
||||||
device_modifications=modifications,
|
|
||||||
)
|
|
||||||
elif result.get_value() == manual_mode:
|
|
||||||
preset_mods = preset.device_modifications if preset else []
|
|
||||||
modifications = _manual_partitioning(preset_mods, devices)
|
|
||||||
|
|
||||||
if modifications:
|
|
||||||
return DiskLayoutConfiguration(
|
|
||||||
config_type=DiskLayoutType.Manual,
|
|
||||||
device_modifications=modifications,
|
|
||||||
)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def select_lvm_config(
|
|
||||||
disk_config: DiskLayoutConfiguration,
|
|
||||||
preset: LvmConfiguration | None = None,
|
|
||||||
) -> LvmConfiguration | None:
|
|
||||||
preset_value = preset.config_type.display_msg() if preset else None
|
|
||||||
default_mode = LvmLayoutType.Default.display_msg()
|
|
||||||
|
|
||||||
items = [MenuItem(default_mode, value=default_mode)]
|
|
||||||
group = MenuItemGroup(items)
|
|
||||||
group.set_focus_by_value(preset_value)
|
|
||||||
|
|
||||||
result = SelectMenu[str](
|
|
||||||
group,
|
|
||||||
allow_reset=True,
|
|
||||||
allow_skip=True,
|
|
||||||
frame=FrameProperties.min(tr('LVM configuration type')),
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Skip:
|
|
||||||
return preset
|
|
||||||
case ResultType.Reset:
|
|
||||||
return None
|
|
||||||
case ResultType.Selection:
|
|
||||||
if result.get_value() == default_mode:
|
|
||||||
return suggest_lvm_layout(disk_config)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _boot_partition(sector_size: SectorSize, using_gpt: bool) -> PartitionModification:
|
|
||||||
flags = [PartitionFlag.BOOT]
|
|
||||||
size = Size(1, Unit.GiB, sector_size)
|
|
||||||
start = Size(1, Unit.MiB, sector_size)
|
|
||||||
if using_gpt:
|
|
||||||
flags.append(PartitionFlag.ESP)
|
|
||||||
|
|
||||||
# boot partition
|
|
||||||
return PartitionModification(
|
|
||||||
status=ModificationStatus.Create,
|
|
||||||
type=PartitionType.Primary,
|
|
||||||
start=start,
|
|
||||||
length=size,
|
|
||||||
mountpoint=Path('/boot'),
|
|
||||||
fs_type=FilesystemType.Fat32,
|
|
||||||
flags=flags,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def select_main_filesystem_format() -> FilesystemType:
|
|
||||||
items = [
|
|
||||||
MenuItem('btrfs', value=FilesystemType.Btrfs),
|
|
||||||
MenuItem('ext4', value=FilesystemType.Ext4),
|
|
||||||
MenuItem('xfs', value=FilesystemType.Xfs),
|
|
||||||
MenuItem('f2fs', value=FilesystemType.F2fs),
|
|
||||||
]
|
|
||||||
|
|
||||||
if arch_config_handler.args.advanced:
|
|
||||||
items.append(MenuItem('ntfs', value=FilesystemType.Ntfs))
|
|
||||||
|
|
||||||
group = MenuItemGroup(items, sort_items=False)
|
|
||||||
result = SelectMenu[FilesystemType](
|
|
||||||
group,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
frame=FrameProperties.min('Filesystem'),
|
|
||||||
allow_skip=False,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Selection:
|
|
||||||
return result.get_value()
|
|
||||||
case _:
|
|
||||||
raise ValueError('Unhandled result type')
|
|
||||||
|
|
||||||
|
|
||||||
def select_mount_options() -> list[str]:
|
|
||||||
prompt = tr('Would you like to use compression or disable CoW?') + '\n'
|
|
||||||
compression = tr('Use compression')
|
|
||||||
disable_cow = tr('Disable Copy-on-Write')
|
|
||||||
|
|
||||||
items = [
|
|
||||||
MenuItem(compression, value=BtrfsMountOption.compress.value),
|
|
||||||
MenuItem(disable_cow, value=BtrfsMountOption.nodatacow.value),
|
|
||||||
]
|
|
||||||
group = MenuItemGroup(items, sort_items=False)
|
|
||||||
result = SelectMenu[str](
|
|
||||||
group,
|
|
||||||
header=prompt,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
columns=2,
|
|
||||||
orientation=Orientation.HORIZONTAL,
|
|
||||||
search_enabled=False,
|
|
||||||
allow_skip=True,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Skip:
|
|
||||||
return []
|
|
||||||
case ResultType.Selection:
|
|
||||||
return [result.get_value()]
|
|
||||||
case _:
|
|
||||||
raise ValueError('Unhandled result type')
|
|
||||||
|
|
||||||
|
|
||||||
def process_root_partition_size(total_size: Size, sector_size: SectorSize) -> Size:
|
|
||||||
# root partition size processing
|
|
||||||
total_device_size = total_size.convert(Unit.GiB)
|
|
||||||
if total_device_size.value > 500:
|
|
||||||
# maximum size
|
|
||||||
return Size(value=50, unit=Unit.GiB, sector_size=sector_size)
|
|
||||||
elif total_device_size.value < 320:
|
|
||||||
# minimum size
|
|
||||||
return Size(value=32, unit=Unit.GiB, sector_size=sector_size)
|
|
||||||
else:
|
|
||||||
# 10% of total size
|
|
||||||
length = total_device_size.value // 10
|
|
||||||
return Size(value=length, unit=Unit.GiB, sector_size=sector_size)
|
|
||||||
|
|
||||||
|
|
||||||
def get_default_btrfs_subvols() -> list[SubvolumeModification]:
|
|
||||||
# https://btrfs.wiki.kernel.org/index.php/FAQ
|
|
||||||
# https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash
|
|
||||||
# https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh
|
|
||||||
return [
|
|
||||||
SubvolumeModification(Path('@'), Path('/')),
|
|
||||||
SubvolumeModification(Path('@home'), Path('/home')),
|
|
||||||
SubvolumeModification(Path('@log'), Path('/var/log')),
|
|
||||||
SubvolumeModification(Path('@pkg'), Path('/var/cache/pacman/pkg')),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def suggest_single_disk_layout(
|
|
||||||
device: BDevice,
|
|
||||||
filesystem_type: FilesystemType | None = None,
|
|
||||||
separate_home: bool | None = None,
|
|
||||||
) -> DeviceModification:
|
|
||||||
if not filesystem_type:
|
|
||||||
filesystem_type = select_main_filesystem_format()
|
|
||||||
|
|
||||||
sector_size = device.device_info.sector_size
|
|
||||||
total_size = device.device_info.total_size
|
|
||||||
available_space = total_size
|
|
||||||
min_size_to_allow_home_part = Size(64, Unit.GiB, sector_size)
|
|
||||||
|
|
||||||
if filesystem_type == FilesystemType.Btrfs:
|
|
||||||
prompt = tr('Would you like to use BTRFS subvolumes with a default structure?') + '\n'
|
|
||||||
group = MenuItemGroup.yes_no()
|
|
||||||
group.set_focus_by_value(MenuItem.yes().value)
|
|
||||||
result = SelectMenu[bool](
|
|
||||||
group,
|
|
||||||
header=prompt,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
columns=2,
|
|
||||||
orientation=Orientation.HORIZONTAL,
|
|
||||||
allow_skip=False,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
using_subvolumes = result.item() == MenuItem.yes()
|
|
||||||
mount_options = select_mount_options()
|
|
||||||
else:
|
|
||||||
using_subvolumes = False
|
|
||||||
mount_options = []
|
|
||||||
|
|
||||||
device_modification = DeviceModification(device, wipe=True)
|
|
||||||
|
|
||||||
using_gpt = device_handler.partition_table.is_gpt()
|
|
||||||
|
|
||||||
if using_gpt:
|
|
||||||
available_space = available_space.gpt_end()
|
|
||||||
|
|
||||||
available_space = available_space.align()
|
|
||||||
|
|
||||||
# Used for reference: https://wiki.archlinux.org/title/partitioning
|
|
||||||
|
|
||||||
boot_partition = _boot_partition(sector_size, using_gpt)
|
|
||||||
device_modification.add_partition(boot_partition)
|
|
||||||
|
|
||||||
if separate_home is False or using_subvolumes or total_size < min_size_to_allow_home_part:
|
|
||||||
using_home_partition = False
|
|
||||||
elif separate_home:
|
|
||||||
using_home_partition = True
|
|
||||||
else:
|
|
||||||
prompt = tr('Would you like to create a separate partition for /home?') + '\n'
|
|
||||||
group = MenuItemGroup.yes_no()
|
|
||||||
group.set_focus_by_value(MenuItem.yes().value)
|
|
||||||
result = SelectMenu(
|
|
||||||
group,
|
|
||||||
header=prompt,
|
|
||||||
orientation=Orientation.HORIZONTAL,
|
|
||||||
columns=2,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
allow_skip=False,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
using_home_partition = result.item() == MenuItem.yes()
|
|
||||||
|
|
||||||
# root partition
|
|
||||||
root_start = boot_partition.start + boot_partition.length
|
|
||||||
|
|
||||||
# Set a size for / (/root)
|
|
||||||
if using_home_partition:
|
|
||||||
root_length = process_root_partition_size(total_size, sector_size)
|
|
||||||
else:
|
|
||||||
root_length = available_space - root_start
|
|
||||||
|
|
||||||
root_partition = PartitionModification(
|
|
||||||
status=ModificationStatus.Create,
|
|
||||||
type=PartitionType.Primary,
|
|
||||||
start=root_start,
|
|
||||||
length=root_length,
|
|
||||||
mountpoint=Path('/') if not using_subvolumes else None,
|
|
||||||
fs_type=filesystem_type,
|
|
||||||
mount_options=mount_options,
|
|
||||||
)
|
|
||||||
|
|
||||||
device_modification.add_partition(root_partition)
|
|
||||||
|
|
||||||
if using_subvolumes:
|
|
||||||
root_partition.btrfs_subvols = get_default_btrfs_subvols()
|
|
||||||
elif using_home_partition:
|
|
||||||
# If we don't want to use subvolumes,
|
|
||||||
# But we want to be able to reuse data between re-installs..
|
|
||||||
# A second partition for /home would be nice if we have the space for it
|
|
||||||
home_start = root_partition.start + root_partition.length
|
|
||||||
home_length = available_space - home_start
|
|
||||||
|
|
||||||
flags = []
|
|
||||||
if using_gpt:
|
|
||||||
flags.append(PartitionFlag.LINUX_HOME)
|
|
||||||
|
|
||||||
home_partition = PartitionModification(
|
|
||||||
status=ModificationStatus.Create,
|
|
||||||
type=PartitionType.Primary,
|
|
||||||
start=home_start,
|
|
||||||
length=home_length,
|
|
||||||
mountpoint=Path('/home'),
|
|
||||||
fs_type=filesystem_type,
|
|
||||||
mount_options=mount_options,
|
|
||||||
flags=flags,
|
|
||||||
)
|
|
||||||
device_modification.add_partition(home_partition)
|
|
||||||
|
|
||||||
return device_modification
|
|
||||||
|
|
||||||
|
|
||||||
def suggest_multi_disk_layout(
|
|
||||||
devices: list[BDevice],
|
|
||||||
filesystem_type: FilesystemType | None = None,
|
|
||||||
) -> list[DeviceModification]:
|
|
||||||
if not devices:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Not really a rock solid foundation of information to stand on, but it's a start:
|
|
||||||
# https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/
|
|
||||||
# https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/
|
|
||||||
min_home_partition_size = Size(40, Unit.GiB, SectorSize.default())
|
|
||||||
# rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size?
|
|
||||||
desired_root_partition_size = Size(32, Unit.GiB, SectorSize.default())
|
|
||||||
mount_options = []
|
|
||||||
|
|
||||||
if not filesystem_type:
|
|
||||||
filesystem_type = select_main_filesystem_format()
|
|
||||||
|
|
||||||
# find proper disk for /home
|
|
||||||
possible_devices = [d for d in devices if d.device_info.total_size >= min_home_partition_size]
|
|
||||||
home_device = max(possible_devices, key=lambda d: d.device_info.total_size) if possible_devices else None
|
|
||||||
|
|
||||||
# find proper device for /root
|
|
||||||
devices_delta = {}
|
|
||||||
for device in devices:
|
|
||||||
if device is not home_device:
|
|
||||||
delta = device.device_info.total_size - desired_root_partition_size
|
|
||||||
devices_delta[device] = delta
|
|
||||||
|
|
||||||
sorted_delta: list[tuple[BDevice, Size]] = sorted(devices_delta.items(), key=lambda x: x[1])
|
|
||||||
root_device: BDevice | None = sorted_delta[0][0]
|
|
||||||
|
|
||||||
if home_device is None or root_device is None:
|
|
||||||
text = tr('The selected drives do not have the minimum capacity required for an automatic suggestion\n')
|
|
||||||
text += tr('Minimum capacity for /home partition: {}GiB\n').format(min_home_partition_size.format_size(Unit.GiB))
|
|
||||||
text += tr('Minimum capacity for Arch Linux partition: {}GiB').format(desired_root_partition_size.format_size(Unit.GiB))
|
|
||||||
|
|
||||||
items = [MenuItem(tr('Continue'))]
|
|
||||||
group = MenuItemGroup(items)
|
|
||||||
SelectMenu(group).run()
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
||||||
if filesystem_type == FilesystemType.Btrfs:
|
|
||||||
mount_options = select_mount_options()
|
|
||||||
|
|
||||||
device_paths = ', '.join([str(d.device_info.path) for d in devices])
|
|
||||||
|
|
||||||
debug(f'Suggesting multi-disk-layout for devices: {device_paths}')
|
|
||||||
debug(f'/root: {root_device.device_info.path}')
|
|
||||||
debug(f'/home: {home_device.device_info.path}')
|
|
||||||
|
|
||||||
root_device_modification = DeviceModification(root_device, wipe=True)
|
|
||||||
home_device_modification = DeviceModification(home_device, wipe=True)
|
|
||||||
|
|
||||||
root_device_sector_size = root_device_modification.device.device_info.sector_size
|
|
||||||
home_device_sector_size = home_device_modification.device.device_info.sector_size
|
|
||||||
|
|
||||||
using_gpt = device_handler.partition_table.is_gpt()
|
|
||||||
|
|
||||||
# add boot partition to the root device
|
|
||||||
boot_partition = _boot_partition(root_device_sector_size, using_gpt)
|
|
||||||
root_device_modification.add_partition(boot_partition)
|
|
||||||
|
|
||||||
root_start = boot_partition.start + boot_partition.length
|
|
||||||
root_length = root_device.device_info.total_size - root_start
|
|
||||||
|
|
||||||
if using_gpt:
|
|
||||||
root_length = root_length.gpt_end()
|
|
||||||
|
|
||||||
root_length = root_length.align()
|
|
||||||
|
|
||||||
# add root partition to the root device
|
|
||||||
root_partition = PartitionModification(
|
|
||||||
status=ModificationStatus.Create,
|
|
||||||
type=PartitionType.Primary,
|
|
||||||
start=root_start,
|
|
||||||
length=root_length,
|
|
||||||
mountpoint=Path('/'),
|
|
||||||
mount_options=mount_options,
|
|
||||||
fs_type=filesystem_type,
|
|
||||||
)
|
|
||||||
root_device_modification.add_partition(root_partition)
|
|
||||||
|
|
||||||
home_start = Size(1, Unit.MiB, home_device_sector_size)
|
|
||||||
home_length = home_device.device_info.total_size - home_start
|
|
||||||
|
|
||||||
flags = []
|
|
||||||
if using_gpt:
|
|
||||||
home_length = home_length.gpt_end()
|
|
||||||
flags.append(PartitionFlag.LINUX_HOME)
|
|
||||||
|
|
||||||
home_length = home_length.align()
|
|
||||||
|
|
||||||
# add home partition to home device
|
|
||||||
home_partition = PartitionModification(
|
|
||||||
status=ModificationStatus.Create,
|
|
||||||
type=PartitionType.Primary,
|
|
||||||
start=home_start,
|
|
||||||
length=home_length,
|
|
||||||
mountpoint=Path('/home'),
|
|
||||||
mount_options=mount_options,
|
|
||||||
fs_type=filesystem_type,
|
|
||||||
flags=flags,
|
|
||||||
)
|
|
||||||
home_device_modification.add_partition(home_partition)
|
|
||||||
|
|
||||||
return [root_device_modification, home_device_modification]
|
|
||||||
|
|
||||||
|
|
||||||
def suggest_lvm_layout(
|
|
||||||
disk_config: DiskLayoutConfiguration,
|
|
||||||
filesystem_type: FilesystemType | None = None,
|
|
||||||
vg_grp_name: str = 'ArchinstallVg',
|
|
||||||
) -> LvmConfiguration:
|
|
||||||
if disk_config.config_type != DiskLayoutType.Default:
|
|
||||||
raise ValueError('LVM suggested volumes are only available for default partitioning')
|
|
||||||
|
|
||||||
using_subvolumes = False
|
|
||||||
btrfs_subvols = []
|
|
||||||
home_volume = True
|
|
||||||
mount_options = []
|
|
||||||
|
|
||||||
if not filesystem_type:
|
|
||||||
filesystem_type = select_main_filesystem_format()
|
|
||||||
|
|
||||||
if filesystem_type == FilesystemType.Btrfs:
|
|
||||||
prompt = tr('Would you like to use BTRFS subvolumes with a default structure?') + '\n'
|
|
||||||
group = MenuItemGroup.yes_no()
|
|
||||||
group.set_focus_by_value(MenuItem.yes().value)
|
|
||||||
|
|
||||||
result = SelectMenu[bool](
|
|
||||||
group,
|
|
||||||
header=prompt,
|
|
||||||
search_enabled=False,
|
|
||||||
allow_skip=False,
|
|
||||||
orientation=Orientation.HORIZONTAL,
|
|
||||||
columns=2,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
using_subvolumes = MenuItem.yes() == result.item()
|
|
||||||
mount_options = select_mount_options()
|
|
||||||
|
|
||||||
if using_subvolumes:
|
|
||||||
btrfs_subvols = get_default_btrfs_subvols()
|
|
||||||
home_volume = False
|
|
||||||
|
|
||||||
boot_part: PartitionModification | None = None
|
|
||||||
other_part: list[PartitionModification] = []
|
|
||||||
|
|
||||||
for mod in disk_config.device_modifications:
|
|
||||||
for part in mod.partitions:
|
|
||||||
if part.is_boot():
|
|
||||||
boot_part = part
|
|
||||||
else:
|
|
||||||
other_part.append(part)
|
|
||||||
|
|
||||||
if not boot_part:
|
|
||||||
raise ValueError('Unable to find boot partition in partition modifications')
|
|
||||||
|
|
||||||
total_vol_available = sum(
|
|
||||||
[p.length for p in other_part],
|
|
||||||
Size(0, Unit.B, SectorSize.default()),
|
|
||||||
)
|
|
||||||
root_vol_size = Size(20, Unit.GiB, SectorSize.default())
|
|
||||||
home_vol_size = total_vol_available - root_vol_size
|
|
||||||
|
|
||||||
lvm_vol_group = LvmVolumeGroup(vg_grp_name, pvs=other_part)
|
|
||||||
|
|
||||||
root_vol = LvmVolume(
|
|
||||||
status=LvmVolumeStatus.Create,
|
|
||||||
name='root',
|
|
||||||
fs_type=filesystem_type,
|
|
||||||
length=root_vol_size,
|
|
||||||
mountpoint=Path('/'),
|
|
||||||
btrfs_subvols=btrfs_subvols,
|
|
||||||
mount_options=mount_options,
|
|
||||||
)
|
|
||||||
|
|
||||||
lvm_vol_group.volumes.append(root_vol)
|
|
||||||
|
|
||||||
if home_volume:
|
|
||||||
home_vol = LvmVolume(
|
|
||||||
status=LvmVolumeStatus.Create,
|
|
||||||
name='home',
|
|
||||||
fs_type=filesystem_type,
|
|
||||||
length=home_vol_size,
|
|
||||||
mountpoint=Path('/home'),
|
|
||||||
)
|
|
||||||
|
|
||||||
lvm_vol_group.volumes.append(home_vol)
|
|
||||||
|
|
||||||
return LvmConfiguration(LvmLayoutType.Default, [lvm_vol_group])
|
|
||||||
|
|
@ -1,305 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import assert_never
|
|
||||||
|
|
||||||
from archinstall.lib.models.packages import Repository
|
|
||||||
from archinstall.lib.packages.packages import list_available_packages
|
|
||||||
from archinstall.lib.translationhandler import tr
|
|
||||||
from archinstall.tui.curses_menu import EditMenu, SelectMenu, Tui
|
|
||||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
|
||||||
from archinstall.tui.result import ResultType
|
|
||||||
from archinstall.tui.types import Alignment, FrameProperties, Orientation, PreviewStyle
|
|
||||||
|
|
||||||
from ..locale.utils import list_timezones
|
|
||||||
from ..models.packages import AvailablePackage, PackageGroup
|
|
||||||
from ..output import warn
|
|
||||||
from ..translationhandler import Language
|
|
||||||
|
|
||||||
|
|
||||||
class PostInstallationAction(Enum):
|
|
||||||
EXIT = tr('Exit archinstall')
|
|
||||||
REBOOT = tr('Reboot system')
|
|
||||||
CHROOT = tr('chroot into installation for post-installation configurations')
|
|
||||||
|
|
||||||
|
|
||||||
def ask_ntp(preset: bool = True) -> bool:
|
|
||||||
header = tr('Would you like to use automatic time synchronization (NTP) with the default time servers?\n') + '\n'
|
|
||||||
header += (
|
|
||||||
tr(
|
|
||||||
'Hardware time and other post-configuration steps might be required in order for NTP to work.\nFor more information, please check the Arch wiki',
|
|
||||||
)
|
|
||||||
+ '\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
preset_val = MenuItem.yes() if preset else MenuItem.no()
|
|
||||||
group = MenuItemGroup.yes_no()
|
|
||||||
group.focus_item = preset_val
|
|
||||||
|
|
||||||
result = SelectMenu[bool](
|
|
||||||
group,
|
|
||||||
header=header,
|
|
||||||
allow_skip=True,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
columns=2,
|
|
||||||
orientation=Orientation.HORIZONTAL,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Skip:
|
|
||||||
return preset
|
|
||||||
case ResultType.Selection:
|
|
||||||
return result.item() == MenuItem.yes()
|
|
||||||
case _:
|
|
||||||
raise ValueError('Unhandled return type')
|
|
||||||
|
|
||||||
|
|
||||||
def ask_hostname(preset: str | None = None) -> str | None:
|
|
||||||
result = EditMenu(
|
|
||||||
tr('Hostname'),
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
allow_skip=True,
|
|
||||||
default_text=preset,
|
|
||||||
).input()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Skip:
|
|
||||||
return preset
|
|
||||||
case ResultType.Selection:
|
|
||||||
hostname = result.text()
|
|
||||||
if len(hostname) < 1:
|
|
||||||
return None
|
|
||||||
return hostname
|
|
||||||
case ResultType.Reset:
|
|
||||||
raise ValueError('Unhandled result type')
|
|
||||||
|
|
||||||
|
|
||||||
def ask_for_a_timezone(preset: str | None = None) -> str | None:
|
|
||||||
default = 'UTC'
|
|
||||||
timezones = list_timezones()
|
|
||||||
|
|
||||||
items = [MenuItem(tz, value=tz) for tz in timezones]
|
|
||||||
group = MenuItemGroup(items, sort_items=True)
|
|
||||||
group.set_selected_by_value(preset)
|
|
||||||
group.set_default_by_value(default)
|
|
||||||
|
|
||||||
result = SelectMenu[str](
|
|
||||||
group,
|
|
||||||
allow_reset=True,
|
|
||||||
allow_skip=True,
|
|
||||||
frame=FrameProperties.min(tr('Timezone')),
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Skip:
|
|
||||||
return preset
|
|
||||||
case ResultType.Reset:
|
|
||||||
return default
|
|
||||||
case ResultType.Selection:
|
|
||||||
return result.get_value()
|
|
||||||
|
|
||||||
|
|
||||||
def select_language(preset: str | None = None) -> str | None:
|
|
||||||
from ..locale.locale_menu import select_kb_layout
|
|
||||||
|
|
||||||
# We'll raise an exception in an upcoming version.
|
|
||||||
# from ..exceptions import Deprecated
|
|
||||||
# raise Deprecated("select_language() has been deprecated, use select_kb_layout() instead.")
|
|
||||||
|
|
||||||
# No need to translate this i feel, as it's a short lived message.
|
|
||||||
warn('select_language() is deprecated, use select_kb_layout() instead. select_language() will be removed in a future version')
|
|
||||||
|
|
||||||
return select_kb_layout(preset)
|
|
||||||
|
|
||||||
|
|
||||||
def select_archinstall_language(languages: list[Language], preset: Language) -> Language:
|
|
||||||
# these are the displayed language names which can either be
|
|
||||||
# the english name of a language or, if present, the
|
|
||||||
# name of the language in its own language
|
|
||||||
|
|
||||||
items = [MenuItem(lang.display_name, lang) for lang in languages]
|
|
||||||
group = MenuItemGroup(items, sort_items=True)
|
|
||||||
group.set_focus_by_value(preset)
|
|
||||||
|
|
||||||
title = 'NOTE: If a language can not displayed properly, a proper font must be set manually in the console.\n'
|
|
||||||
title += 'All available fonts can be found in "/usr/share/kbd/consolefonts"\n'
|
|
||||||
title += 'e.g. setfont LatGrkCyr-8x16 (to display latin/greek/cyrillic characters)\n'
|
|
||||||
|
|
||||||
result = SelectMenu[Language](
|
|
||||||
group,
|
|
||||||
header=title,
|
|
||||||
allow_skip=True,
|
|
||||||
allow_reset=False,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
frame=FrameProperties.min(header=tr('Select language')),
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Skip:
|
|
||||||
return preset
|
|
||||||
case ResultType.Selection:
|
|
||||||
return result.get_value()
|
|
||||||
case ResultType.Reset:
|
|
||||||
raise ValueError('Language selection not handled')
|
|
||||||
|
|
||||||
|
|
||||||
def ask_additional_packages_to_install(
|
|
||||||
preset: list[str] = [],
|
|
||||||
repositories: set[Repository] = set(),
|
|
||||||
) -> list[str]:
|
|
||||||
repositories |= {Repository.Core, Repository.Extra}
|
|
||||||
|
|
||||||
respos_text = ', '.join([r.value for r in repositories])
|
|
||||||
output = tr('Repositories: {}').format(respos_text) + '\n'
|
|
||||||
|
|
||||||
output += tr('Loading packages...')
|
|
||||||
Tui.print(output, clear_screen=True)
|
|
||||||
|
|
||||||
packages = list_available_packages(tuple(repositories))
|
|
||||||
package_groups = PackageGroup.from_available_packages(packages)
|
|
||||||
|
|
||||||
# Additional packages (with some light weight error handling for invalid package names)
|
|
||||||
header = tr('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.') + '\n'
|
|
||||||
header += tr('Select any packages from the below list that should be installed additionally') + '\n'
|
|
||||||
|
|
||||||
# there are over 15k packages so this needs to be quick
|
|
||||||
preset_packages: list[AvailablePackage | PackageGroup] = []
|
|
||||||
for p in preset:
|
|
||||||
if p in packages:
|
|
||||||
preset_packages.append(packages[p])
|
|
||||||
elif p in package_groups:
|
|
||||||
preset_packages.append(package_groups[p])
|
|
||||||
|
|
||||||
items = [
|
|
||||||
MenuItem(
|
|
||||||
name,
|
|
||||||
value=pkg,
|
|
||||||
preview_action=lambda x: x.value.info(),
|
|
||||||
)
|
|
||||||
for name, pkg in packages.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
items += [
|
|
||||||
MenuItem(
|
|
||||||
name,
|
|
||||||
value=group,
|
|
||||||
preview_action=lambda x: x.value.info(),
|
|
||||||
)
|
|
||||||
for name, group in package_groups.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
menu_group = MenuItemGroup(items, sort_items=True)
|
|
||||||
menu_group.set_selected_by_value(preset_packages)
|
|
||||||
|
|
||||||
result = SelectMenu[AvailablePackage | PackageGroup](
|
|
||||||
menu_group,
|
|
||||||
header=header,
|
|
||||||
alignment=Alignment.LEFT,
|
|
||||||
allow_reset=True,
|
|
||||||
allow_skip=True,
|
|
||||||
multi=True,
|
|
||||||
preview_frame=FrameProperties.max('Package info'),
|
|
||||||
preview_style=PreviewStyle.RIGHT,
|
|
||||||
preview_size='auto',
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Skip:
|
|
||||||
return preset
|
|
||||||
case ResultType.Reset:
|
|
||||||
return []
|
|
||||||
case ResultType.Selection:
|
|
||||||
selected_pacakges = result.get_values()
|
|
||||||
return [pkg.name for pkg in selected_pacakges]
|
|
||||||
|
|
||||||
|
|
||||||
def add_number_of_parallel_downloads(preset: int | None = None) -> int | None:
|
|
||||||
max_recommended = 5
|
|
||||||
|
|
||||||
header = tr('This option enables the number of parallel downloads that can occur during package downloads') + '\n'
|
|
||||||
header += tr('Enter the number of parallel downloads to be enabled.\n\nNote:\n')
|
|
||||||
header += tr(' - Maximum recommended value : {} ( Allows {} parallel downloads at a time )').format(max_recommended, max_recommended) + '\n'
|
|
||||||
header += tr(' - Disable/Default : 0 ( Disables parallel downloading, allows only 1 download at a time )\n')
|
|
||||||
|
|
||||||
def validator(s: str | None) -> str | None:
|
|
||||||
if s is not None:
|
|
||||||
try:
|
|
||||||
value = int(s)
|
|
||||||
if value >= 0:
|
|
||||||
return None
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return tr('Invalid download number')
|
|
||||||
|
|
||||||
result = EditMenu(
|
|
||||||
tr('Number downloads'),
|
|
||||||
header=header,
|
|
||||||
allow_skip=True,
|
|
||||||
allow_reset=True,
|
|
||||||
validator=validator,
|
|
||||||
default_text=str(preset) if preset is not None else None,
|
|
||||||
).input()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Skip:
|
|
||||||
return preset
|
|
||||||
case ResultType.Reset:
|
|
||||||
return 0
|
|
||||||
case ResultType.Selection:
|
|
||||||
downloads: int = int(result.text())
|
|
||||||
case _:
|
|
||||||
assert_never(result.type_)
|
|
||||||
|
|
||||||
pacman_conf_path = Path('/etc/pacman.conf')
|
|
||||||
with pacman_conf_path.open() as f:
|
|
||||||
pacman_conf = f.read().split('\n')
|
|
||||||
|
|
||||||
with pacman_conf_path.open('w') as fwrite:
|
|
||||||
for line in pacman_conf:
|
|
||||||
if 'ParallelDownloads' in line:
|
|
||||||
fwrite.write(f'ParallelDownloads = {downloads}\n')
|
|
||||||
else:
|
|
||||||
fwrite.write(f'{line}\n')
|
|
||||||
|
|
||||||
return downloads
|
|
||||||
|
|
||||||
|
|
||||||
def ask_post_installation() -> PostInstallationAction:
|
|
||||||
header = tr('Installation completed') + '\n\n'
|
|
||||||
header += tr('What would you like to do next?') + '\n'
|
|
||||||
|
|
||||||
items = [MenuItem(action.value, value=action) for action in PostInstallationAction]
|
|
||||||
group = MenuItemGroup(items)
|
|
||||||
|
|
||||||
result = SelectMenu[PostInstallationAction](
|
|
||||||
group,
|
|
||||||
header=header,
|
|
||||||
allow_skip=False,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Selection:
|
|
||||||
return result.get_value()
|
|
||||||
case _:
|
|
||||||
raise ValueError('Post installation action not handled')
|
|
||||||
|
|
||||||
|
|
||||||
def ask_abort() -> None:
|
|
||||||
prompt = tr('Do you really want to abort?') + '\n'
|
|
||||||
group = MenuItemGroup.yes_no()
|
|
||||||
|
|
||||||
result = SelectMenu[bool](
|
|
||||||
group,
|
|
||||||
header=prompt,
|
|
||||||
allow_skip=False,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
columns=2,
|
|
||||||
orientation=Orientation.HORIZONTAL,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
if result.item() == MenuItem.yes():
|
|
||||||
exit(0)
|
|
||||||
|
|
@ -1,188 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from archinstall.lib.translationhandler import tr
|
|
||||||
from archinstall.tui.curses_menu import SelectMenu
|
|
||||||
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
|
|
||||||
from archinstall.tui.result import ResultType
|
|
||||||
from archinstall.tui.types import Alignment, FrameProperties, FrameStyle, Orientation, PreviewStyle
|
|
||||||
|
|
||||||
from ..args import arch_config_handler
|
|
||||||
from ..hardware import GfxDriver, SysInfo
|
|
||||||
from ..models.bootloader import Bootloader
|
|
||||||
|
|
||||||
|
|
||||||
def select_kernel(preset: list[str] = []) -> list[str]:
|
|
||||||
"""
|
|
||||||
Asks the user to select a kernel for system.
|
|
||||||
|
|
||||||
:return: The string as a selected kernel
|
|
||||||
:rtype: string
|
|
||||||
"""
|
|
||||||
kernels = ['linux', 'linux-lts', 'linux-zen', 'linux-hardened']
|
|
||||||
default_kernel = 'linux'
|
|
||||||
|
|
||||||
items = [MenuItem(k, value=k) for k in kernels]
|
|
||||||
|
|
||||||
group = MenuItemGroup(items, sort_items=True)
|
|
||||||
group.set_default_by_value(default_kernel)
|
|
||||||
group.set_focus_by_value(default_kernel)
|
|
||||||
group.set_selected_by_value(preset)
|
|
||||||
|
|
||||||
result = SelectMenu[str](
|
|
||||||
group,
|
|
||||||
allow_skip=True,
|
|
||||||
allow_reset=True,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
frame=FrameProperties.min(tr('Kernel')),
|
|
||||||
multi=True,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Skip:
|
|
||||||
return preset
|
|
||||||
case ResultType.Reset:
|
|
||||||
return []
|
|
||||||
case ResultType.Selection:
|
|
||||||
return result.get_values()
|
|
||||||
|
|
||||||
|
|
||||||
def ask_for_bootloader(preset: Bootloader | None) -> Bootloader | None:
|
|
||||||
# Systemd is UEFI only
|
|
||||||
options = []
|
|
||||||
hidden_options = []
|
|
||||||
default = None
|
|
||||||
header = None
|
|
||||||
|
|
||||||
if arch_config_handler.args.skip_boot:
|
|
||||||
default = Bootloader.NO_BOOTLOADER
|
|
||||||
else:
|
|
||||||
hidden_options += [Bootloader.NO_BOOTLOADER]
|
|
||||||
|
|
||||||
if not SysInfo.has_uefi():
|
|
||||||
options += [Bootloader.Grub, Bootloader.Limine]
|
|
||||||
if not default:
|
|
||||||
default = Bootloader.Grub
|
|
||||||
header = tr('UEFI is not detected and some options are disabled')
|
|
||||||
else:
|
|
||||||
options += [b for b in Bootloader if b not in hidden_options]
|
|
||||||
if not default:
|
|
||||||
default = Bootloader.Systemd
|
|
||||||
|
|
||||||
items = [MenuItem(o.value, value=o) for o in options]
|
|
||||||
group = MenuItemGroup(items)
|
|
||||||
group.set_default_by_value(default)
|
|
||||||
group.set_focus_by_value(preset)
|
|
||||||
|
|
||||||
result = SelectMenu[Bootloader](
|
|
||||||
group,
|
|
||||||
header=header,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
frame=FrameProperties.min(tr('Bootloader')),
|
|
||||||
allow_skip=True,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Skip:
|
|
||||||
return preset
|
|
||||||
case ResultType.Selection:
|
|
||||||
return result.get_value()
|
|
||||||
case ResultType.Reset:
|
|
||||||
raise ValueError('Unhandled result type')
|
|
||||||
|
|
||||||
|
|
||||||
def ask_for_uki(preset: bool = True) -> bool:
|
|
||||||
prompt = tr('Would you like to use unified kernel images?') + '\n'
|
|
||||||
|
|
||||||
group = MenuItemGroup.yes_no()
|
|
||||||
group.set_focus_by_value(preset)
|
|
||||||
|
|
||||||
result = SelectMenu[bool](
|
|
||||||
group,
|
|
||||||
header=prompt,
|
|
||||||
columns=2,
|
|
||||||
orientation=Orientation.HORIZONTAL,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
allow_skip=True,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Skip:
|
|
||||||
return preset
|
|
||||||
case ResultType.Selection:
|
|
||||||
return result.item() == MenuItem.yes()
|
|
||||||
case ResultType.Reset:
|
|
||||||
raise ValueError('Unhandled result type')
|
|
||||||
|
|
||||||
|
|
||||||
def select_driver(options: list[GfxDriver] = [], preset: GfxDriver | None = None) -> GfxDriver | None:
|
|
||||||
"""
|
|
||||||
Some what convoluted function, whose job is simple.
|
|
||||||
Select a graphics driver from a pre-defined set of popular options.
|
|
||||||
|
|
||||||
(The template xorg is for beginner users, not advanced, and should
|
|
||||||
there for appeal to the general public first and edge cases later)
|
|
||||||
"""
|
|
||||||
if not options:
|
|
||||||
options = [driver for driver in GfxDriver]
|
|
||||||
|
|
||||||
items = [MenuItem(o.value, value=o, preview_action=lambda x: x.value.packages_text()) for o in options]
|
|
||||||
group = MenuItemGroup(items, sort_items=True)
|
|
||||||
group.set_default_by_value(GfxDriver.AllOpenSource)
|
|
||||||
|
|
||||||
if preset is not None:
|
|
||||||
group.set_focus_by_value(preset)
|
|
||||||
|
|
||||||
header = ''
|
|
||||||
if SysInfo.has_amd_graphics():
|
|
||||||
header += tr('For the best compatibility with your AMD hardware, you may want to use either the all open-source or AMD / ATI options.') + '\n'
|
|
||||||
if SysInfo.has_intel_graphics():
|
|
||||||
header += tr('For the best compatibility with your Intel hardware, you may want to use either the all open-source or Intel options.\n')
|
|
||||||
if SysInfo.has_nvidia_graphics():
|
|
||||||
header += tr('For the best compatibility with your Nvidia hardware, you may want to use the Nvidia proprietary driver.\n')
|
|
||||||
|
|
||||||
result = SelectMenu[GfxDriver](
|
|
||||||
group,
|
|
||||||
header=header,
|
|
||||||
allow_skip=True,
|
|
||||||
allow_reset=True,
|
|
||||||
preview_size='auto',
|
|
||||||
preview_style=PreviewStyle.BOTTOM,
|
|
||||||
preview_frame=FrameProperties(tr('Info'), h_frame_style=FrameStyle.MIN),
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Skip:
|
|
||||||
return preset
|
|
||||||
case ResultType.Reset:
|
|
||||||
return None
|
|
||||||
case ResultType.Selection:
|
|
||||||
return result.get_value()
|
|
||||||
|
|
||||||
|
|
||||||
def ask_for_swap(preset: bool = True) -> bool:
|
|
||||||
if preset:
|
|
||||||
default_item = MenuItem.yes()
|
|
||||||
else:
|
|
||||||
default_item = MenuItem.no()
|
|
||||||
|
|
||||||
prompt = tr('Would you like to use swap on zram?') + '\n'
|
|
||||||
|
|
||||||
group = MenuItemGroup.yes_no()
|
|
||||||
group.set_focus_by_value(default_item)
|
|
||||||
|
|
||||||
result = SelectMenu[bool](
|
|
||||||
group,
|
|
||||||
header=prompt,
|
|
||||||
columns=2,
|
|
||||||
orientation=Orientation.HORIZONTAL,
|
|
||||||
alignment=Alignment.CENTER,
|
|
||||||
allow_skip=True,
|
|
||||||
).run()
|
|
||||||
|
|
||||||
match result.type_:
|
|
||||||
case ResultType.Skip:
|
|
||||||
return preset
|
|
||||||
case ResultType.Selection:
|
|
||||||
return result.item() == MenuItem.yes()
|
|
||||||
case ResultType.Reset:
|
|
||||||
raise ValueError('Unhandled result type')
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue