mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-07-13 22:31:07 +00:00
Compare commits
896 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2e013a33b | |||
| da0853b6f1 | |||
| 7db38ff9e4 | |||
| 92ac036f78 | |||
| aef7fa5f93 | |||
| c412b08135 | |||
| a3b6372c11 | |||
| 78288befb8 | |||
| 49cd3d43c8 | |||
| d9b52806c0 | |||
| 774db2d780 | |||
| 68afda4c71 | |||
| fa9fa73078 | |||
| 3e1e24cb50 | |||
| 18fe38c30b | |||
| 6ee8b26bd5 | |||
| fce49f22c9 | |||
| af8e2c9e9b | |||
| 1c312bb528 | |||
| e39194e9f6 | |||
| 21cc029c18 | |||
| 40671b99d5 | |||
| 3ad2c494af | |||
| 34014d1cdd | |||
| 93ed2b864b | |||
| cca931ccd0 | |||
| 5e090cebdb | |||
| d7984c12f0 | |||
| 96684d7ddd | |||
| 2e9837b70d | |||
| ac4a8fb2cd | |||
| 1db8eb0ac4 | |||
| dc394f7df9 | |||
| 058f784b05 | |||
| f688768ca7 | |||
| a09ad7617d | |||
| 81aeb56ba3 | |||
| 2cd4ef5e86 | |||
| 998ed48dde | |||
| 021d88dc4c | |||
| 9012ee7144 | |||
| 945ddb2942 | |||
| 9cd0926588 | |||
| f5c12d03ec | |||
| 606ee2fb0e | |||
| 875e9cf58a | |||
| a809a4b67f | |||
| 437ae2c16c | |||
| 10661f3127 | |||
| 88ac3d82a0 | |||
| 386765ab4e | |||
| 78998182b8 | |||
| 2563391aaf | |||
| 90a78e99e7 | |||
| a05eab9042 | |||
| db46147f0d | |||
| 49ebbc34fa | |||
| e376f1307f | |||
| 415fcf58ce | |||
| 17a2d6362c | |||
| e6275de4ed | |||
| 4d009cba6d | |||
| f6defbf90d | |||
| c4fefa6071 | |||
| b0f1828ae7 | |||
| dec025b45a | |||
| 89008e5350 | |||
| 422196d413 | |||
| 6fe2eade26 | |||
| 5266f54257 | |||
| bbf9e38fda | |||
| ba80a91d95 | |||
| 536d040a6a | |||
| bed8752f3a | |||
| 4093ca8986 | |||
| f027155885 | |||
| 443d4ae667 | |||
| c8f7fa8c51 | |||
| 93c36fb429 | |||
| 2d6d42f969 | |||
| 6a2454548d | |||
| 389bad6725 | |||
| 5738b8b911 | |||
| 5ac2e3de19 | |||
| 799dc73d8a | |||
| 4e79cbf71a | |||
| 4fa5d55317 | |||
| a7fa3b90e4 | |||
| ce07cda8ab | |||
| 00c4f32294 | |||
| 49cf91ea52 | |||
| 4a8430dc67 | |||
| 46af782db2 | |||
| 6443e02352 | |||
| 999ad39d6f | |||
| dfab5f56b2 | |||
| 10798b9ba3 | |||
| 358e3dc4d2 | |||
| c13cd029bc | |||
| ae32cc8fbb | |||
| dff5b775a9 | |||
| db3f20546e | |||
| 53368468a4 | |||
| 228c2cce51 | |||
| f5aec4e5c1 | |||
| 9217c8c759 | |||
| 6392520e06 | |||
| c6306631e6 | |||
| 97b906c536 | |||
| 435375721d | |||
| 4c5caba6b7 | |||
| b83df9d2c5 | |||
| f2ea76aab9 | |||
| 471b1c1331 | |||
| bd770aac2f | |||
| 6abe35ef8c | |||
| fdc27a9ebf | |||
| b729096a25 | |||
| 390b9da29e | |||
| 256376df85 | |||
| 939a94d889 | |||
| 2b1b17a1a3 | |||
| 9e6705056a | |||
| b3a3a81f70 | |||
| 3e5dbbd6cd | |||
| f41e44895d | |||
| 765bbf486f | |||
| a3c54afb82 | |||
| 7f223ecc0a | |||
| 7769a4a6e0 | |||
| 066d1b1dde | |||
| 1f22a27360 | |||
| 75682bc7be | |||
| e5d824b03f | |||
| 8d0d597473 | |||
| 995b396360 | |||
| 7f813cf0c3 | |||
| d4eb55ef95 | |||
| 09350e88ab | |||
| 2feaa14f46 | |||
| 9653fc4f4a | |||
| bcd46c66e8 | |||
| 6ea56faede | |||
| 9e346530f2 | |||
| d283dccc1e | |||
| 8a4e900ab9 | |||
| fa6cf8ce36 | |||
| a706fbb751 | |||
| 9a23f5c79d | |||
| aaab9069bf | |||
| f00b575641 | |||
| 6f57ed550b | |||
| 08640d9108 | |||
| 65324633b4 | |||
| ed67898012 | |||
| a1a8dd68e8 | |||
| a9505386c2 | |||
| a07b20bf50 | |||
| ed70897c39 | |||
| 0423c3e67c | |||
| 571f62327f | |||
| 286ff4bcef | |||
| 0660c33de3 | |||
| c8421e97ee | |||
| bc2288afc1 | |||
| 503c8b0355 | |||
| 6738f9206d | |||
| f865e998b0 | |||
| 4880ca4fee | |||
| 56114ecc1e | |||
| 57ab3ffb8f | |||
| 8b08bfe3b4 | |||
| e34356989d | |||
| 7c7804a9f4 | |||
| c4a56f1454 | |||
| 1caed156ad | |||
| 45a620c40b | |||
| 3c1fdec0e9 | |||
| f5d7085325 | |||
| 0cc35e70e3 | |||
| f09082dff2 | |||
| 8d53a59a6a | |||
| 20e7ba3b1d | |||
| 4b5a645f8d | |||
| 6a3c16a646 | |||
| 93ce7f9a51 | |||
| 6fe77eb465 | |||
| 7c6c24a46d | |||
| f48993ccd5 | |||
| 22600a9eac | |||
| b167df904b | |||
| cd0ac7a7bd | |||
| 910d178c71 | |||
| 1e7d4daf18 | |||
| 7bc4810377 | |||
| aa66e76db7 | |||
| e0f9e38f44 | |||
| d6cdb5bea5 | |||
| 6d157ca809 | |||
| 5486ae4a19 | |||
| 8bdf420cdc | |||
| 2deca6d715 | |||
| 180adf3f33 | |||
| 634054926d | |||
| 53baaac61e | |||
| d2a93a94e3 | |||
| 7134aea125 | |||
| 740d537618 | |||
| 9f78e79f1f | |||
| 09456141f5 | |||
| 9e30e98e90 | |||
| df2e98a1d3 | |||
| 24993047c6 | |||
| b20e844cc9 | |||
| ce75ccb405 | |||
| 1acff852d0 | |||
| 62320e8ec6 | |||
| 1089bab526 | |||
| 0e0012a6e0 | |||
| fbbb80d091 | |||
| 5316427371 | |||
| 0fa748f302 | |||
| 7a3d32dcfa | |||
| a396126a79 | |||
| 9785835c0e | |||
| c4f4e37731 | |||
| f43ee2fd1d | |||
| f7a50e5294 | |||
| ddd3c3f06f | |||
| 242f3d4d34 | |||
| 303f9ff03c | |||
| 65fac09d8d | |||
| aaf7adbac1 | |||
| 9e011990ee | |||
| 529d4caa0e | |||
| d7c4fccf98 | |||
| 1e30838be4 | |||
| d57276f214 | |||
| 31e59df2c8 | |||
| 23cd843e44 | |||
| c08a292070 | |||
| 5163aa4dca | |||
| 69f0966ff1 | |||
| 16308dc3ae | |||
| 962599e937 | |||
| 758a81e3db | |||
| 84942e3c98 | |||
| 6f8b1509d7 | |||
| b4372ff583 | |||
| 41fc7bffbb | |||
| 300f5026c4 | |||
| 10aa91a033 | |||
| 4169851eaa | |||
| 4d9e06156d | |||
| af2269c64a | |||
| f44fa19c42 | |||
| fd3c6343f1 | |||
| 9edff2826f | |||
| b38ecfb35c | |||
| 3c7c748b4a | |||
| 434057ec49 | |||
| a01b090c2b | |||
| 8ffc1299f0 | |||
| 02b13de7f4 | |||
| 50a045434d | |||
| 7bbe3242d4 | |||
| a9cb357182 | |||
| feec9141b2 | |||
| 148ef09e70 | |||
| 5ff635db70 | |||
| 9df5820bce | |||
| b7d3493ae3 | |||
| f8b725d175 | |||
| 46b3c82aca | |||
| 0d34432c9a | |||
| 3cd9cff4ee | |||
| 9e02d7a38f | |||
| 9819ed761e | |||
| 99ca0cb2fd | |||
| ea3cb6e766 | |||
| 0ae4941321 | |||
| 855b55237b | |||
| 59d14665a5 | |||
| 414eb93b1e | |||
| 14721913a4 | |||
| f68e48fb47 | |||
| 77cceb4c10 | |||
| eca22fdaed | |||
| 719af3676e | |||
| 6c63ad6df5 | |||
| 497a115535 | |||
| 91598951c5 | |||
| f364e00bea | |||
| 87c87d6455 | |||
| 265de3dc72 | |||
| d06c02a1c7 | |||
| 29bb6284b0 | |||
| 70b7fcf47a | |||
| 706808fc97 | |||
| 7e5f21c26d | |||
| 9a7f55605a | |||
| 6085e88f5e | |||
| 56e97040d6 | |||
| 9510a14b90 | |||
| 6eeef39fe5 | |||
| 0e6434faad | |||
| 768132bb6c | |||
| f1095fe007 | |||
| 1af04448c9 | |||
| aad607eaef | |||
| 3347212bf2 | |||
| f3e9fbf9d0 | |||
| c3dacd6883 | |||
| 624b4619f1 | |||
| 215fe7a592 | |||
| dd8d5d130b | |||
| b4fa10781b | |||
| f6cdd806b2 | |||
| cf9a1e7bec | |||
| 1b93f4f5e0 | |||
| a872ecfc23 | |||
| c130f52163 | |||
| 5fb93bd331 | |||
| 58e702213d | |||
| e784032bc6 | |||
| c54b14b833 | |||
| e61b246216 | |||
| 2a9eab5f1a | |||
| acc204de6d | |||
| 0991dbb59c | |||
| 18d17d4d52 | |||
| de7184fc3a | |||
| 2d21c999d1 | |||
| ff78577d61 | |||
| eb6eda578c | |||
| e823fd3f27 | |||
| e4a857dad0 | |||
| fc8f6c2985 | |||
| bb6414f9d4 | |||
| 064ac6811c | |||
| 7d16142b3b | |||
| 69a79a2e64 | |||
| 5b4bff9c93 | |||
| 3f8b9eaed6 | |||
| deab8ddae6 | |||
| eacb6ec729 | |||
| dc2eebce39 | |||
| 6b755b4828 | |||
| a73341e885 | |||
| 969aa78c95 | |||
| df787657aa | |||
| eec94521a7 | |||
| b18000c777 | |||
| dbaff5f981 | |||
| ae84ca0c56 | |||
| 8f047f9a96 | |||
| 54bd016c17 | |||
| 42f77742f8 | |||
| 46bb9c95d0 | |||
| 5dd4763a80 | |||
| 45eba6a6e3 | |||
| 4c1d0abb85 | |||
| 1e00bf9398 | |||
| bf9a46936c | |||
| a0b17981e3 | |||
| c440f5bbd5 | |||
| 8ff567cac3 | |||
| 9fe760efdf | |||
| bc9682373d | |||
| aedcbf1985 | |||
| 26b991bf47 | |||
| 8bf422b02a | |||
| 0c920832c9 | |||
| bef8d33570 | |||
| b540ced77f | |||
| 723b6971dd | |||
| 59356e905a | |||
| 99eecdebf3 | |||
| 5e42dd4e70 | |||
| 4ec7e2d129 | |||
| 9a521af078 | |||
| d1132b49fc | |||
| 2c21ae26c3 | |||
| bba90b3eb8 | |||
| 3cac53ac11 | |||
| 9fa6722eaa | |||
| 1d4ed0e0c3 | |||
| 2cda4a2469 | |||
| 9406537a15 | |||
| 598af7d9db | |||
| 6530afbfc7 | |||
| ad1c0051c4 | |||
| 5b172ad20b | |||
| 42fccb76ef | |||
| c37f67c558 | |||
| 89d9b581bd | |||
| 116dc0b329 | |||
| e3ed36fafd | |||
| 573ade4ba5 | |||
| 9259d9c727 | |||
| c863ee063c | |||
| 480c3a54b8 | |||
| 3b3ef43863 | |||
| 368db86dde | |||
| adf6a0a952 | |||
| 575a618096 | |||
| 2c03354d4a | |||
| 4008aa2e0a | |||
| 9cb39f6767 | |||
| c7de182f3d | |||
| d21c3fdf9f | |||
| 9f2fe219c8 | |||
| ad570aae0f | |||
| 116b65d92a | |||
| 5e5171cf3e | |||
| af3360e84a | |||
| 95f52f7ebe | |||
| f5fe200247 | |||
| a1c12200b8 | |||
| 09839f755a | |||
| fcd4d60cad | |||
| 3681473942 | |||
| 4dd5a1e82e | |||
| cbd1d3d5b8 | |||
| e3347aec2d | |||
| 3ad6cd27c6 | |||
| 54a68279be | |||
| d99091a3b4 | |||
| be6cf5b8af | |||
| 91e548569d | |||
| 6c3b2ef266 | |||
| 785607c70b | |||
| d22a3152db | |||
| adb5b58a1d | |||
| 8b2601beaa | |||
| 9116a4370e | |||
| da93fdc77a | |||
| 8bbd643551 | |||
| 6834dd442c | |||
| d23e3cbbea | |||
| 6bc6bdaeae | |||
| 7458c73c7d | |||
| 2cecbb3d53 | |||
| c73a6c7bae | |||
| 0b70b5af45 | |||
| fde75a14c2 | |||
| 1e0d1a5a32 | |||
| 75919637e8 | |||
| 467d109cfc | |||
| a131c77699 | |||
| 10fbc31d2a | |||
| 7e0d12b8b1 | |||
| 7ab00ee579 | |||
| 2d62148ee6 | |||
| c1f2d5ff4a | |||
| 2888649ced | |||
| 63855f5964 | |||
| 8bf665d7d3 | |||
| a7ac77ce4d | |||
| fc01bf3d1c | |||
| d81e91e117 | |||
| 57f62ba163 | |||
| ded896ee1b | |||
| 1fdcea0524 | |||
| a93f43dcd0 | |||
| 20974dae6f | |||
| cbcfff27b8 | |||
| 96f394bab0 | |||
| aae3c82f3d | |||
| ab9e1fb1bd | |||
| 82322f7a6c | |||
| 125da217d3 | |||
| d113c65c81 | |||
| 050041b2de | |||
| e0ed636d7e | |||
| 61feb2ebc7 | |||
| da1c6b0101 | |||
| c1718b3862 | |||
| 34fe8128aa | |||
| a78c48ed4d | |||
| e368396653 | |||
| f4abd83568 | |||
| e6888ba788 | |||
| a78050c1bf | |||
| 93876307f8 | |||
| 4d482520cd | |||
| 461fbd40ce | |||
| 419da3db89 | |||
| 00ca06ffab | |||
| 5a05c8ce91 | |||
| b09aea13af | |||
| 04a52f759d | |||
| ef6cf0f00b | |||
| ab650b7417 | |||
| 7ca5b3a363 | |||
| 666fa98075 | |||
| 684ef33dbc | |||
| cf35addaa5 | |||
| 11732a8609 | |||
| 64cc8fd6b0 | |||
| 4202751e9c | |||
| 41515f6c95 | |||
| ac92b506e0 | |||
| 5bbd1ad9a9 | |||
| 541d8d9b39 | |||
| e41dd09a8a | |||
| 8e65263b90 | |||
| afc488255c | |||
| effc120a43 | |||
| 4b27e102ef | |||
| e0126bb811 | |||
| 8c04dc4c2a | |||
| 83e9d7c523 | |||
| 9be4a89c08 | |||
| 0f8596db1e | |||
| c403c45d72 | |||
| 00dbd90953 | |||
| 3e299d9685 | |||
| e20637f7ad | |||
| 5a09d46f9a | |||
| 90be3276dd | |||
| a4f646eb2a | |||
| ebd06cb443 | |||
| fe66c6c45c | |||
| bba58352e0 | |||
| 41cc58ed31 | |||
| 20e45845ba | |||
| 9fa1fa108f | |||
| 577bd9e5f8 | |||
| 20b9c1fa4a | |||
| a7c9183aa0 | |||
| 9e0dd3ae97 | |||
| f4e4e88e82 | |||
| 2684e4b3f4 | |||
| 45f5006db3 | |||
| b26aa2145f | |||
| 4462eba860 | |||
| f2c23bad17 | |||
| 1603086b96 | |||
| b6f6ee8c76 | |||
| b71d1f6e3d | |||
| 7883746e53 | |||
| ae9118654d | |||
| 81fa73f725 | |||
| 14cb548c3b | |||
| 2eb93a6090 | |||
| dd6ef38c99 | |||
| 415dc42313 | |||
| 3e1af17796 | |||
| 880cebe54f | |||
| cbcc796a3a | |||
| dc8a52f6c0 | |||
| 34c93e53cc | |||
| f7ec24523b | |||
| f817b32976 | |||
| 9fc2b681d4 | |||
| 59ff0589ab | |||
| 1b53de6f2b | |||
| 41080c5ff8 | |||
| 791ce4f242 | |||
| b75bd30658 | |||
| b3b3cad706 | |||
| 82b932371b | |||
| 45fb2f3c46 | |||
| 4a5b9d9fa1 | |||
| 2f8316ecb0 | |||
| 77ad35fce8 | |||
| 48da4646cf | |||
| 3d98dd267a | |||
| 09678a3ae1 | |||
| 8e2732f6fe | |||
| 73e311a41c | |||
| 1e8388af5d | |||
| c1d74726b7 | |||
| f2ddcc6d23 | |||
| a5ce6b78dd | |||
| 1a83dd6f5a | |||
| 16a33f7729 | |||
| fb21b1da53 | |||
| 8394004943 | |||
| 75c6bf6119 | |||
| 7e20ff5184 | |||
| 757614f5a0 | |||
| 43166e5f94 | |||
| 43696b9920 | |||
| 8befee58fe | |||
| 9d016f51b5 | |||
| c5fbccd519 | |||
| eae5748a30 | |||
| 695ef708b6 | |||
| 26408e82c0 | |||
| 7b647a9b5a | |||
| fac228d6c6 | |||
| eb5ac5a52b | |||
| 047925dcfe | |||
| b8d2775050 | |||
| bf959ceb5f | |||
| 945b6d0ac0 | |||
| 5df9f30e37 | |||
| 1ea3911531 | |||
| d7966e419d | |||
| 3a5268993e | |||
| ad84895e18 | |||
| b1dfafe275 | |||
| 375f9fcfb7 | |||
| 04dcaa93ab | |||
| f42c2c57fb | |||
| dc3562a4ec | |||
| dda94f8f9e | |||
| 709db6a5ed | |||
| 89a2d8e298 | |||
| b9cd98235e | |||
| 1905360f8f | |||
| a7018d9f7d | |||
| 73057bc08d | |||
| 7f87863734 | |||
| 2dac5a2520 | |||
| 720edc24ba | |||
| fe9711d24f | |||
| c73a2a4aa9 | |||
| 571f720ae2 | |||
| 5030395025 | |||
| 03633d47ea | |||
| 1a575d6db8 | |||
| cdc018ad07 | |||
| f806c8918e | |||
| 4daff81d5f | |||
| 06bd29b78d | |||
| c67ac3730c | |||
| c094d4ee79 | |||
| 3cb479ba4b | |||
| 307a7f4a71 | |||
| 25734876de | |||
| 53df735eb2 | |||
| fb7275f9dd | |||
| f01af5f54a | |||
| 1abe8d291f | |||
| 1a83e55d64 | |||
| b8e17c4879 | |||
| bee41dcc55 | |||
| 9f7995fb2f | |||
| 9ce1c36f35 | |||
| 4990ce4198 | |||
| 8f18ead4cc | |||
| 7ea50887a4 | |||
| 97ec5bfa41 | |||
| 5fcc15191a | |||
| c5f086a7c6 | |||
| 432ca0cc48 | |||
| 1a0322b32e | |||
| fc086c0d87 | |||
| d5503b22ba | |||
| a132b1544a | |||
| 63acae4b1c | |||
| 8e6473d2a0 | |||
| fdcbcc4541 | |||
| 83931f5cf4 | |||
| d4eadf0013 | |||
| 04174a3e6d | |||
| 041b3824c1 | |||
| a11cce43ff | |||
| 1388df9a7e | |||
| 6ee1c8ef5a | |||
| a7c6d95b34 | |||
| 060c7412b1 | |||
| 93406483fe | |||
| 046febc440 | |||
| 9964a96296 | |||
| 9529525cb5 | |||
| fa4c1c6205 | |||
| f9b1d89756 | |||
| 9197b416e6 | |||
| f54a2fe740 | |||
| d1f990eac8 | |||
| 1e85c1db70 | |||
| 32aaa4a1f8 | |||
| a9e20f0958 | |||
| 32b129a806 | |||
| 2752e68ade | |||
| 5b678149dc | |||
| 79e1cff421 | |||
| 2e07de4a12 | |||
| 7e3ef889e8 | |||
| 52b54a9d05 | |||
| c0f7f81050 | |||
| 38af344bde | |||
| 62496478ae | |||
| ce48169aad | |||
| 21845bc934 | |||
| 249003de0a | |||
| d8704e9c75 | |||
| 0a8b172e9c | |||
| 7d782f120d | |||
| 9057ecf67a | |||
| 35df354c14 | |||
| 4d4e5e9dde | |||
| ff24188ca1 | |||
| 9d4f85624d | |||
| 295f810222 | |||
| d8523bd83b | |||
| be017ed102 | |||
| c73f24f8c6 | |||
| 0d806e3471 | |||
| 20962f0385 | |||
| fd38dfd176 | |||
| 5f7f58041d | |||
| 233b1f7f39 | |||
| a83f1d5aa5 | |||
| be09c04e78 | |||
| bfbbb987ad | |||
| 04f365f1a6 | |||
| 86bd49ed1b | |||
| f6ad609616 | |||
| 2f5790f69f | |||
| faaf72a840 | |||
| ef44895b0d | |||
| 5cfffbcd46 | |||
| 3e0058620b | |||
| e897e2cde2 | |||
| edef4944f6 | |||
| a4eaf87116 | |||
| 1192f12f91 | |||
| 444da87fdc | |||
| 6be6614e6d | |||
| 8add507957 | |||
| af5363d158 | |||
| d60c553ea2 | |||
| 5a6d0e0000 | |||
| 427ba0f0ea | |||
| 266d2bd77d | |||
| f333e89bd1 | |||
| 659db071a1 | |||
| 56c642e2a9 | |||
| 845bc9b5e8 | |||
| 57f45fdc89 | |||
| ba483173af | |||
| 6d1f641e50 | |||
| 0cf7756ec4 | |||
| b561bcc25d | |||
| 25d76b0482 | |||
| d2fe4b044f | |||
| 22272167e6 | |||
| 38dce8c0fe | |||
| 68aa327670 | |||
| 04f6af58d9 | |||
| 41c8f4c35f | |||
| 168b2f6880 | |||
| 14e8eee986 | |||
| 88c8c929dc | |||
| 5bb244cbe8 | |||
| f3fd0780fb | |||
| 11c03a9041 | |||
| 214f319123 | |||
| cfbc9d3267 | |||
| 0f4c89145b | |||
| 49a05d0520 | |||
| 19d1e17727 | |||
| d7bf647493 | |||
| 19ba2ba8e5 | |||
| f49894107a | |||
| 233ba9c415 | |||
| b7cff0a19d | |||
| 60b94b9ac0 | |||
| 284f449225 | |||
| d18df0935e | |||
| c15cc4b853 | |||
| fc67778e89 | |||
| 39ac9d4e1f | |||
| bc923b0e8f | |||
| 2842e40bf7 | |||
| 343768e015 | |||
| 8f55cc600e | |||
| c757554148 | |||
| 899603e4b9 | |||
| f488105fd2 | |||
| 2e89b9483f | |||
| d3a00e1937 | |||
| d9b4015be5 | |||
| c6b550761b | |||
| 0129ec39ec | |||
| 62661c9fb1 | |||
| 78636c2035 | |||
| 47c4e5bb42 | |||
| d548fa02f0 | |||
| b5c6c286f7 | |||
| aad599be67 | |||
| f8ff2cbf5b | |||
| dad3f418ad | |||
| c455bd34ce | |||
| 69b319d39a | |||
| 33cfa8fef8 | |||
| 9d37e3ae23 | |||
| 2eb2d3a70a | |||
| 46389b87e7 | |||
| 856a94ff00 | |||
| a0e6009876 | |||
| f2b39fe439 | |||
| 213b2c65a0 | |||
| a416214e5f | |||
| e5966c497f | |||
| fbccb1394d | |||
| 01887ec44b | |||
| 4d4ddffa8f | |||
| 4729c0ea5f | |||
| 035a23a8de | |||
| 0d37ce5095 | |||
| 70465b5044 | |||
| 81afa23d98 | |||
| 1b979725a3 | |||
| c2c7ee6292 | |||
| a3fe361596 | |||
| 20d13f1c10 | |||
| 6c96c9e73a | |||
| 9b08b0694e | |||
| fcb892f39f | |||
| 1785b0470f | |||
| c04f940ad3 | |||
| 207da4caa7 | |||
| cad85b0f20 | |||
| 3e8e54857f | |||
| 6470b13511 | |||
| 4e08297311 | |||
| 32c3c52874 | |||
| e637a647c5 | |||
| e21b990777 | |||
| c0be7f7a17 | |||
| 612cb2fa22 | |||
| cf276f2398 | |||
| 4ca2348f0d | |||
| eb02e1e62b | |||
| 8d7d508bb2 | |||
| a875225b78 | |||
| 5449dec32a | |||
| eb7f3b2484 | |||
| e87402fdde | |||
| 3996055f56 | |||
| 74fc283078 | |||
| eb92f75384 | |||
| 63529633ec | |||
| 930b47c704 | |||
| a89888769f | |||
| b6bc1b77dc | |||
| 4a462502e7 | |||
| f383b04754 | |||
| d744fe1647 | |||
| 6f1b37c4cb | |||
| 5cb3fd5853 | |||
| 60b8477cde | |||
| 69499b2d0a | |||
| 475afe4e08 | |||
| 2f2d6c2b70 | |||
| 15e3d2500c | |||
| 3d74b1485a | |||
| 413d3b7509 | |||
| 3e2fb7b4e6 | |||
| 71196dc58b | |||
| e7736e985f | |||
| f929a552e8 | |||
| 2c7ef3471e | |||
| 47bb22b1f4 | |||
| 5a340146bb | |||
| 0937a9a4b5 | |||
| 310eba694f | |||
| 22d2057c3a | |||
| 47fc5bca57 | |||
| d4222eca25 | |||
| b5046b787c | |||
| 75c0cc970e | |||
| 504d57b2f5 | |||
| 4c20d0241a | |||
| db0a6bf34e | |||
| 8f5af7965e | |||
| f35278e978 | |||
| a288986450 | |||
| 2cef540cc0 | |||
| 7fd22e9f16 | |||
| e2608db991 | |||
| 2a0311b042 | |||
| 0fcb46afca | |||
| 374b3febc8 | |||
| 3e0b3cdbaa | |||
| 663e822a92 | |||
| d3e79120cb | |||
| 67b97a64ea | |||
| 7ace74af44 | |||
| b7d481858d | |||
| f753563804 | |||
| 4727894349 | |||
| 4b98b21a70 | |||
| 9410c521a1 | |||
| dd42cd0cd6 | |||
| 50b409cd3e | |||
| 356cd35c5f | |||
| 3405105dce | |||
| 4445c8c871 | |||
| a3a66c7c9a |
@@ -0,0 +1,3 @@
|
|||||||
|
skips:
|
||||||
|
- B404
|
||||||
|
- B603
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.eggs/
|
||||||
|
.git/
|
||||||
|
.github/
|
||||||
|
.idea/
|
||||||
|
.mypy_cache/
|
||||||
|
.pytest_cache/
|
||||||
|
.tox/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
*.egg-info/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyd
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
### Steps to reproduce
|
||||||
|
|
||||||
|
Steps to reproduce the behavior (commands, environment etc).
|
||||||
|
|
||||||
|
### Expected behavior
|
||||||
|
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
Add logs to help explain your problem. By default, the application writes logs into `/dev/log` which is usually default systemd journal and can be accessed by `journalctl` command.
|
||||||
|
|
||||||
|
You can also attach any additional information which can be helpful, e.g. configuration used by the application (be aware of passwords and other secrets if any); it can be generated by using `ahriman config` command.
|
||||||
|
|
||||||
|
It is also sometimes useful to have information about installed packages which can be accessed by `ahriman version` command.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Security report
|
||||||
|
about: Create a report related to security issues
|
||||||
|
title: ''
|
||||||
|
labels: security
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
A clear and concise description of what the issue is.
|
||||||
|
|
||||||
|
### Steps to reproduce
|
||||||
|
|
||||||
|
Steps to reproduce the behavior (commands, environment etc).
|
||||||
|
|
||||||
|
### Intended impact
|
||||||
|
|
||||||
|
Brief optional description of how this vulnerability can be used and which effects can be achieved.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature summary
|
||||||
|
|
||||||
|
Brief description of the feature required
|
||||||
|
|
||||||
|
### Cause of the feature request
|
||||||
|
|
||||||
|
A clear and concise description of what the problem is. E.g. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
### Proposed changes and/or features
|
||||||
|
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: Question
|
||||||
|
about: Create an issue to get help with project
|
||||||
|
title: ''
|
||||||
|
labels: question
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Describe your question below
|
||||||
|
|
||||||
|
A clear and concise description of your issue for which you would like to get help.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
## Summary
|
||||||
|
|
||||||
|
Brief description of the pull request. Try to provide clear explanation for major changes.
|
||||||
|
|
||||||
|
Please make sure that branch called either `feature/feature-name` for feature-related pull requests or `bug/bug-name` for bug-related ones.
|
||||||
|
|
||||||
|
Put `closes #ISSUE` in case if the pull requests solves one of the opened issues.
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
|
||||||
|
- [ ] Tests to cover new code
|
||||||
|
- [ ] `tox` passed
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
name: Docker image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
- '!*rc*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker-image:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Set image date
|
||||||
|
id: args
|
||||||
|
run: echo "::set-output name=date::$(date -d yesterday +'%Y-%m-%d')"
|
||||||
|
|
||||||
|
- name: Login to docker hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to github container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
arcan1s/ahriman
|
||||||
|
ghcr.io/arcan1s/ahriman
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{raw}}
|
||||||
|
type=edge
|
||||||
|
|
||||||
|
- name: Build an image and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
build-args: |
|
||||||
|
BUILD_DATE=${{ steps.args.outputs.date }}
|
||||||
|
file: docker/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
name: Regress
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: 1 0 * * 0
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-regress-tests:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
services:
|
||||||
|
ahriman:
|
||||||
|
image: arcan1s/ahriman:edge
|
||||||
|
env:
|
||||||
|
AHRIMAN_PORT: 8080
|
||||||
|
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman-web.sock
|
||||||
|
options: --privileged --entrypoint entrypoint-web
|
||||||
|
ports:
|
||||||
|
- 8080
|
||||||
|
volumes:
|
||||||
|
- repo:/var/lib/ahriman
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: arcan1s/ahriman:edge
|
||||||
|
env:
|
||||||
|
AHRIMAN_DEBUG: y
|
||||||
|
AHRIMAN_OUTPUT: console
|
||||||
|
AHRIMAN_PORT: 8080
|
||||||
|
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman-web.sock
|
||||||
|
options: --privileged
|
||||||
|
volumes:
|
||||||
|
- repo:/var/lib/ahriman
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- run: pacman -Sy
|
||||||
|
|
||||||
|
- name: Init repository
|
||||||
|
run: entrypoint help
|
||||||
|
|
||||||
|
- name: Print configuration
|
||||||
|
run: |
|
||||||
|
ahriman \
|
||||||
|
--log-handler "$AHRIMAN_OUTPUT" \
|
||||||
|
service-config
|
||||||
|
|
||||||
|
- name: Validate configuration
|
||||||
|
run: |
|
||||||
|
ahriman \
|
||||||
|
--log-handler "$AHRIMAN_OUTPUT" \
|
||||||
|
service-config-validate
|
||||||
|
|
||||||
|
- name: Create a user
|
||||||
|
run: |
|
||||||
|
sudo -u ahriman ahriman \
|
||||||
|
--log-handler "$AHRIMAN_OUTPUT" \
|
||||||
|
user-add \
|
||||||
|
--packager "github actions <actions@github.com>" \
|
||||||
|
--password ahriman \
|
||||||
|
--role full \
|
||||||
|
ahriman
|
||||||
|
|
||||||
|
- name: Fetch users
|
||||||
|
run: |
|
||||||
|
ahriman \
|
||||||
|
--log-handler "$AHRIMAN_OUTPUT" \
|
||||||
|
user-list \
|
||||||
|
--exit-code
|
||||||
|
|
||||||
|
- name: Add package
|
||||||
|
run: |
|
||||||
|
sudo -u ahriman ahriman \
|
||||||
|
--log-handler "$AHRIMAN_OUTPUT" \
|
||||||
|
package-add \
|
||||||
|
--exit-code \
|
||||||
|
--now \
|
||||||
|
--refresh \
|
||||||
|
ahriman
|
||||||
|
|
||||||
|
- name: Update status of the package
|
||||||
|
run: |
|
||||||
|
ahriman \
|
||||||
|
--log-handler "$AHRIMAN_OUTPUT" \
|
||||||
|
package-status-update \
|
||||||
|
--status failed \
|
||||||
|
ahriman
|
||||||
|
|
||||||
|
- name: Request status of the package
|
||||||
|
run: |
|
||||||
|
ahriman \
|
||||||
|
--log-handler "$AHRIMAN_OUTPUT" \
|
||||||
|
package-status \
|
||||||
|
--exit-code \
|
||||||
|
--info \
|
||||||
|
--status failed \
|
||||||
|
ahriman
|
||||||
|
|
||||||
|
- name: Update packages
|
||||||
|
run: |
|
||||||
|
sudo -u ahriman ahriman \
|
||||||
|
--log-handler "$AHRIMAN_OUTPUT" \
|
||||||
|
repo-update \
|
||||||
|
--exit-code \
|
||||||
|
|| true
|
||||||
|
|
||||||
|
- name: Add patch
|
||||||
|
run: |
|
||||||
|
echo '${pkgver%%.*}' | \
|
||||||
|
sudo -u ahriman ahriman \
|
||||||
|
--log-handler "$AHRIMAN_OUTPUT" \
|
||||||
|
patch-add \
|
||||||
|
ahriman \
|
||||||
|
pkgrel
|
||||||
|
|
||||||
|
- name: Retrieve patches
|
||||||
|
run: |
|
||||||
|
ahriman \
|
||||||
|
--log-handler "$AHRIMAN_OUTPUT" \
|
||||||
|
patch-list \
|
||||||
|
--exit-code \
|
||||||
|
ahriman
|
||||||
|
|
||||||
|
- name: Rebuild packages
|
||||||
|
run: |
|
||||||
|
sudo -u ahriman ahriman \
|
||||||
|
--log-handler "$AHRIMAN_OUTPUT" \
|
||||||
|
repo-rebuild \
|
||||||
|
--depends-on python \
|
||||||
|
--exit-code
|
||||||
|
|
||||||
|
- name: Remove package
|
||||||
|
run: |
|
||||||
|
sudo -u ahriman ahriman \
|
||||||
|
--log-handler "$AHRIMAN_OUTPUT" \
|
||||||
|
package-remove \
|
||||||
|
ahriman
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
make-release:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: archlinux:base
|
||||||
|
options: -w /build
|
||||||
|
volumes:
|
||||||
|
- ${{ github.workspace }}:/build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- run: pacman --noconfirm -Syu base-devel git python-tox
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Extract version
|
||||||
|
id: version
|
||||||
|
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
|
||||||
|
|
||||||
|
- name: Create changelog
|
||||||
|
id: changelog
|
||||||
|
uses: jaywcjlove/changelog-generator@main
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
filter: 'Release \d+\.\d+\.\d+'
|
||||||
|
|
||||||
|
- name: Create archive
|
||||||
|
run: tox -e archive
|
||||||
|
env:
|
||||||
|
VERSION: ${{ steps.version.outputs.VERSION }}
|
||||||
|
|
||||||
|
- name: Publish release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
body: |
|
||||||
|
${{ steps.changelog.outputs.compareurl }}
|
||||||
|
${{ steps.changelog.outputs.changelog }}
|
||||||
|
files: dist/ahriman-*.tar.gz
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
Executable
+65
@@ -0,0 +1,65 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Install the package and run main install commands
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
[[ $1 = "minimal" ]] && MINIMAL_INSTALL=1
|
||||||
|
|
||||||
|
# install dependencies
|
||||||
|
echo -e '[arcanisrepo]\nServer = https://repo.arcanis.me/$arch\nSigLevel = Never' | tee -a /etc/pacman.conf
|
||||||
|
# refresh the image
|
||||||
|
pacman -Syyu --noconfirm
|
||||||
|
# main dependencies
|
||||||
|
pacman -S --noconfirm devtools git npm pyalpm python-bcrypt python-filelock python-inflection python-pyelftools python-requests python-systemd sudo
|
||||||
|
# make dependencies
|
||||||
|
pacman -S --noconfirm --asdeps base-devel python-build python-hatchling python-installer python-tox python-wheel
|
||||||
|
# optional dependencies
|
||||||
|
if [[ -z $MINIMAL_INSTALL ]]; then
|
||||||
|
# web server
|
||||||
|
pacman -S --noconfirm python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-aiohttp-sse-git python-cryptography python-jinja
|
||||||
|
# additional features
|
||||||
|
pacman -S --noconfirm gnupg ipython python-boto3 python-cerberus python-matplotlib rsync
|
||||||
|
fi
|
||||||
|
# FIXME since 1.0.4 devtools requires dbus to be run, which doesn't work now in container
|
||||||
|
cp "docker/systemd-nspawn.sh" "/usr/local/bin/systemd-nspawn"
|
||||||
|
|
||||||
|
# create fresh tarball
|
||||||
|
tox -e archive
|
||||||
|
# run makepkg
|
||||||
|
PKGVER=$(python -c "from src.ahriman import __version__; print(__version__)")
|
||||||
|
mv "dist/ahriman-$PKGVER.tar.gz" package/archlinux
|
||||||
|
chmod +777 package/archlinux # because fuck you that's why
|
||||||
|
cd package/archlinux
|
||||||
|
sudo -u nobody -- makepkg -cf --skipchecksums --noconfirm
|
||||||
|
sudo -u nobody -- makepkg --packagelist | grep "ahriman-core-$PKGVER" | pacman -U --noconfirm --nodeps -
|
||||||
|
if [[ -z $MINIMAL_INSTALL ]]; then
|
||||||
|
sudo -u nobody -- makepkg --packagelist | grep "ahriman-triggers-$PKGVER" | pacman -U --noconfirm --nodeps -
|
||||||
|
sudo -u nobody -- makepkg --packagelist | grep "ahriman-web-$PKGVER" | pacman -U --noconfirm --nodeps -
|
||||||
|
fi
|
||||||
|
# create machine-id which is required by build tools
|
||||||
|
systemd-machine-id-setup
|
||||||
|
|
||||||
|
# remove unused dependencies
|
||||||
|
pacman -Qdtq | pacman -Rscn --noconfirm -
|
||||||
|
|
||||||
|
# initial setup command as root
|
||||||
|
[[ -z $MINIMAL_INSTALL ]] && WEB_ARGS=("--web-port" "8080")
|
||||||
|
ahriman -a x86_64 -r "github" service-setup --packager "ahriman bot <ahriman@example.com>" "${WEB_ARGS[@]}"
|
||||||
|
# enable services
|
||||||
|
systemctl enable ahriman@x86_64-github.timer
|
||||||
|
if [[ -z $MINIMAL_INSTALL ]]; then
|
||||||
|
# validate configuration
|
||||||
|
ahriman service-config-validate --exit-code
|
||||||
|
# run web service (detached)
|
||||||
|
systemctl enable ahriman-web
|
||||||
|
sudo -u ahriman -- ahriman web &
|
||||||
|
WEB_PID=$!
|
||||||
|
fi
|
||||||
|
# add the first package
|
||||||
|
sudo -u ahriman -- ahriman --log-handler console package-add --now ahriman
|
||||||
|
# check if package was actually installed
|
||||||
|
test -n "$(find "/var/lib/ahriman/repository/github/x86_64" -name "ahriman*pkg*")"
|
||||||
|
# run package check
|
||||||
|
sudo -u ahriman -- ahriman repo-update
|
||||||
|
# stop web service lol
|
||||||
|
[[ -z $WEB_PID ]] || kill $WEB_PID
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
name: Setup
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-setup-minimal:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: archlinux:base
|
||||||
|
options: --privileged -w /build
|
||||||
|
volumes:
|
||||||
|
- ${{ github.workspace }}:/build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup the minimal service in arch linux container
|
||||||
|
run: .github/workflows/setup.sh minimal
|
||||||
|
|
||||||
|
run-setup:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: archlinux:base
|
||||||
|
volumes:
|
||||||
|
- ${{ github.workspace }}:/build
|
||||||
|
options: --privileged -w /build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup the service in arch linux container
|
||||||
|
run: .github/workflows/setup.sh
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
schedule:
|
||||||
|
- cron: 1 0 * * *
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-tests:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: archlinux:base
|
||||||
|
options: -w /build
|
||||||
|
volumes:
|
||||||
|
- ${{ github.workspace }}:/build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- run: pacman --noconfirm -Syu base-devel git npm python-tox
|
||||||
|
|
||||||
|
- run: git config --global --add safe.directory *
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run check and tests
|
||||||
|
run: tox
|
||||||
|
|
||||||
|
- name: Generate documentation and check if there are untracked changes
|
||||||
|
run: |
|
||||||
|
tox -e docs
|
||||||
|
[ -z "$(git status --porcelain docs/*.rst)" ]
|
||||||
+14
@@ -94,3 +94,17 @@ ENV/
|
|||||||
.venv/
|
.venv/
|
||||||
|
|
||||||
*.tar.xz
|
*.tar.xz
|
||||||
|
status_cache.json
|
||||||
|
|
||||||
|
*.db
|
||||||
|
|
||||||
|
docs/html/
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
ahriman-web/package/share/ahriman/templates/static/index.js
|
||||||
|
ahriman-web/package/share/ahriman/templates/static/index.css
|
||||||
|
|
||||||
|
# local configs
|
||||||
|
/*.ini
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[tool.pylint.main]
|
||||||
|
init-hook = "sys.path.append('tools')"
|
||||||
|
load-plugins = [
|
||||||
|
"pylint.extensions.docparams",
|
||||||
|
"pylint.extensions.bad_builtin",
|
||||||
|
"pylint_plugins.definition_order",
|
||||||
|
"pylint_plugins.import_order",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pylint.classes]
|
||||||
|
bad-functions = [
|
||||||
|
"print",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pylint.design]
|
||||||
|
max-parents = 15
|
||||||
|
|
||||||
|
[tool.pylint."messages control"]
|
||||||
|
disable = [
|
||||||
|
"raw-checker-failed",
|
||||||
|
"bad-inline-option",
|
||||||
|
"locally-disabled",
|
||||||
|
"file-ignored",
|
||||||
|
"suppressed-message",
|
||||||
|
"useless-suppression",
|
||||||
|
"deprecated-pragma",
|
||||||
|
"use-symbolic-message-instead",
|
||||||
|
"use-implicit-booleaness-not-comparison-to-string",
|
||||||
|
"use-implicit-booleaness-not-comparison-to-zero",
|
||||||
|
"missing-module-docstring",
|
||||||
|
"line-too-long",
|
||||||
|
"no-name-in-module",
|
||||||
|
"import-outside-toplevel",
|
||||||
|
"invalid-name",
|
||||||
|
"raise-missing-from",
|
||||||
|
"wrong-import-order",
|
||||||
|
"too-few-public-methods",
|
||||||
|
"too-many-instance-attributes",
|
||||||
|
"broad-exception-caught",
|
||||||
|
"fixme",
|
||||||
|
"too-many-arguments",
|
||||||
|
"duplicate-code",
|
||||||
|
"cyclic-import",
|
||||||
|
"too-many-positional-arguments",
|
||||||
|
]
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[pytest]
|
||||||
|
addopts = --cov=ahriman --cov-report=term-missing:skip-covered --no-cov-on-fail --cov-fail-under=100 --spec
|
||||||
|
asyncio_default_fixture_loop_scope = function
|
||||||
|
asyncio_mode = auto
|
||||||
|
pythonpath = tests
|
||||||
|
resource-path.directory-name-test-resources = ../../tests/testresources
|
||||||
|
spec_test_format = {result} {docstring_summary}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
build:
|
||||||
|
os: ubuntu-lts-latest
|
||||||
|
tools:
|
||||||
|
python: "3.13"
|
||||||
|
apt_packages:
|
||||||
|
- graphviz
|
||||||
|
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- requirements: docs/requirements.txt
|
||||||
|
|
||||||
|
formats:
|
||||||
|
- pdf
|
||||||
|
|
||||||
|
sphinx:
|
||||||
|
builder: html
|
||||||
|
configuration: docs/conf.py
|
||||||
|
fail_on_warning: true
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Current maintainer:
|
||||||
|
Evgenii Alekseev <esalexeev (at) gmail (dot) com>
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
# ahriman configuration
|
|
||||||
|
|
||||||
Some groups can be specified for each architecture separately. E.g. if there are `build` and `build_x86_64` groups it will use the `build_x86_64` for the `x86_64` architecture and `build` for any other (architecture specific group has higher priority).
|
|
||||||
|
|
||||||
## `settings` group
|
|
||||||
|
|
||||||
Base configuration settings.
|
|
||||||
|
|
||||||
* `include` - path to directory with configuration files overrides, string, required.
|
|
||||||
* `logging` - path to logging configuration, string, required. Check `logging.ini` for reference.
|
|
||||||
|
|
||||||
## `alpm` group
|
|
||||||
|
|
||||||
libalpm and AUR related configuration.
|
|
||||||
|
|
||||||
* `aur_url` - base url for AUR, string, required.
|
|
||||||
* `database` - path to pacman local database cache, string, required.
|
|
||||||
* `repositories` - list of pacman repositories, space separated list of strings, required.
|
|
||||||
* `root` - root for alpm library, string, required.
|
|
||||||
|
|
||||||
## `build_*` groups
|
|
||||||
|
|
||||||
Build related configuration. Group name must refer to architecture, e.g. it should be `build_x86_64` for x86_64 architecture.
|
|
||||||
|
|
||||||
* `archbuild_flags` - additional flags passed to `archbuild` command, space separated list of strings, optional.
|
|
||||||
* `build_command` - default build command, string, required.
|
|
||||||
* `ignore_packages` - list packages to ignore during a regular update (manual update will still work), space separated list of strings, optional.
|
|
||||||
* `makepkg_flags` - additional flags passed to `makepkg` command, space separated list of strings, optional.
|
|
||||||
* `makechrootpkg_flags` - additional flags passed to `makechrootpkg` command, space separated list of strings, optional.
|
|
||||||
|
|
||||||
## `repository` group
|
|
||||||
|
|
||||||
Base repository settings.
|
|
||||||
|
|
||||||
* `name` - repository name, string, required.
|
|
||||||
* `root` - root path for application, string, required.
|
|
||||||
|
|
||||||
## `sign_*` groups
|
|
||||||
|
|
||||||
Settings for signing packages or repository. Group name must refer to architecture, e.g. it should be `sign_x86_64` for x86_64 architecture.
|
|
||||||
|
|
||||||
* `target` - configuration flag to enable signing, space separated list of strings, required. Allowed values are `package` (sign each package separately), `repository` (sign repository database file).
|
|
||||||
* `key` - default PGP key, string, required. This key will also be used for database signing if enabled.
|
|
||||||
* `key_*` settings - PGP key which will be used for specific packages, string, optional. For example, if there is `key_yay` option the specified key will be used for yay package and default key for others.
|
|
||||||
|
|
||||||
## `report` group
|
|
||||||
|
|
||||||
Report generation settings.
|
|
||||||
|
|
||||||
* `target` - list of reports to be generated, space separated list of strings, optional. Allowed values are `html`.
|
|
||||||
|
|
||||||
### `html_*` groups
|
|
||||||
|
|
||||||
Group name must refer to architecture, e.g. it should be `html_x86_64` for x86_64 architecture.
|
|
||||||
|
|
||||||
* `path` - path to html report file, string, required.
|
|
||||||
* `homepage` - link to homepage, string, optional.
|
|
||||||
* `link_path` - prefix for HTML links, string, required.
|
|
||||||
* `template_path` - path to Jinja2 template, string, required.
|
|
||||||
|
|
||||||
## `upload` group
|
|
||||||
|
|
||||||
Remote synchronization settings.
|
|
||||||
|
|
||||||
* `target` - list of synchronizations to be used, space separated list of strings, optional. Allowed values are `rsync`, `s3`.
|
|
||||||
|
|
||||||
### `rsync_*` groups
|
|
||||||
|
|
||||||
Group name must refer to architecture, e.g. it should be `rsync_x86_64` for x86_64 architecture. Requires `rsync` package to be installed. Do not forget to configure ssh for user `ahriman`.
|
|
||||||
|
|
||||||
* `remote` - remote server to rsync (e.g. `1.2.3.4:5678:path/to/sync`), string, required.
|
|
||||||
|
|
||||||
### `s3_*` groups
|
|
||||||
|
|
||||||
Group name must refer to architecture, e.g. it should be `s3_x86_64` for x86_64 architecture. Requires `aws-cli` package to be installed. Do not forget to configure it for user `ahriman`.
|
|
||||||
|
|
||||||
* `bucket` - bucket name (e.g. `s3://bucket/path`), string, required.
|
|
||||||
|
|
||||||
## `web_*` groups
|
|
||||||
|
|
||||||
Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name must refer to architecture, e.g. it should be `web_x86_64` for x86_64 architecture.
|
|
||||||
|
|
||||||
* `host` - host to bind, string, optional.
|
|
||||||
* `port` - port to bind, int, optional.
|
|
||||||
* `templates` - path to templates directory, string, required.
|
|
||||||
+286
@@ -0,0 +1,286 @@
|
|||||||
|
# Contributing to ahriman
|
||||||
|
|
||||||
|
Welcome to ahriman! The goal of the project is to provide the best user experience to manage Arch linux repositories. In order to follow this we set some limitations for the issue creations and heavily restricted code contribution.
|
||||||
|
|
||||||
|
## Create an issue
|
||||||
|
|
||||||
|
Basically just follow the suggested templates:
|
||||||
|
|
||||||
|
* Bug report requires at least the way to reproduce the issue and behaviour description (expected and actual ones). In order to resolve the bug, the additional questions may be asked, please consider them as lesser evil.
|
||||||
|
* Feature requests basically just require feature description and the purpose why do you want this feature to be implemented. It is required to make sure that the feature you want is going to be implemented in the way you really want it (and to make sure that this feature is not already implemented).
|
||||||
|
* Questions and discussions have free templates, and you are free to ask your question in the way you want.
|
||||||
|
|
||||||
|
## Code contribution
|
||||||
|
|
||||||
|
There are some strict limitation for suggested pull requests:
|
||||||
|
|
||||||
|
* `autopep8`, `bandit`, `pylint`, `mypy` must pass.
|
||||||
|
* Test coverage must remain 100%.
|
||||||
|
|
||||||
|
### Code formatting
|
||||||
|
|
||||||
|
In order to resolve all difficult cases the `autopep8` is used. You can perform formatting at any time by running `tox` or running `autopep8` command directly.
|
||||||
|
|
||||||
|
### Code style
|
||||||
|
|
||||||
|
Again, the most checks can be performed by `tox` command, though some additional guidelines must be applied:
|
||||||
|
|
||||||
|
* Every class, every function (including private and protected), every attribute must be documented. The project follows [Google style documentation](https://google.github.io/styleguide/pyguide.html). The only exception is local functions.
|
||||||
|
* Correct way to document function (if a section is empty, e.g. no notes or there are no args, it should be omitted) is the following:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def foo(argument: str, *, flag: bool = False) -> int:
|
||||||
|
"""
|
||||||
|
do foo. With very very very long
|
||||||
|
docstring
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
Very important note about this function
|
||||||
|
Probably multi-line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
argument(str): an argument. This argument has
|
||||||
|
long description also
|
||||||
|
flag(bool, optional): a flag (Default value = False)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: result with
|
||||||
|
long description
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: a local function error occurs
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Very informative example how to use this function, e.g.::
|
||||||
|
|
||||||
|
>>> foo("argument", flag=False)
|
||||||
|
|
||||||
|
Note that function documentation is in rST.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
`Returns` should be replaced with `Yields` for generators.
|
||||||
|
|
||||||
|
Class attributes should be documented in the following way:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Clazz(BaseClazz):
|
||||||
|
"""
|
||||||
|
brand-new implementation of :class:`BaseClazz`
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
CLAZZ_ATTRIBUTE(int): (class attribute) a brand-new class attribute
|
||||||
|
instance_attribute(str): an instance attribute
|
||||||
|
with the long description
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Very informative class usage example, e.g.::
|
||||||
|
|
||||||
|
>>> from module import Clazz
|
||||||
|
>>> clazz = Clazz()
|
||||||
|
"""
|
||||||
|
|
||||||
|
CLAZZ_ATTRIBUTE: ClassVar[int] = 42
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
*args(Any): positional arguments
|
||||||
|
**kwargs(Any): keyword arguments
|
||||||
|
"""
|
||||||
|
self.instance_attribute = ""
|
||||||
|
```
|
||||||
|
|
||||||
|
Note missing comment for the `__init__` method, which is the special case.
|
||||||
|
|
||||||
|
* Type annotations are the must, even for local functions. For the function argument `self` (for instance methods) and `cls` (for class methods) should not be annotated.
|
||||||
|
* For collection types built-in classes must be used if possible (e.g. `dict` instead of `typing.Dict`, `tuple` instead of `typing.Tuple`). In case if built-in type is not available, but `collections.abc` provides interface, it must be used (e.g. `collections.abc.Awaitable` instead of `typing.Awaitable`, `collections.abc.Iterable` instead of `typing.Iterable`). For union classes, the bar operator (`|`) must be used (e.g. `float | int` instead of `typing.Union[float, int]`), which also includes `typing.Optional` (e.g. `str | None` instead of `Optional[str]`).
|
||||||
|
* `classmethod` should (almost) always return `Self`. In case of mypy warning (e.g. if there is a branch in which function doesn't return the instance of `cls`) consider using `staticmethod` instead.
|
||||||
|
* Class attributes must be decorated as `ClassVar[...]`.
|
||||||
|
* Recommended order of function definitions in class:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Clazz:
|
||||||
|
|
||||||
|
def __init__(self) -> None: ... # replace with `__post_init__` for dataclasses
|
||||||
|
|
||||||
|
@property
|
||||||
|
def property(self) -> Any: ...
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def property_cached(self) -> Any: ... # cached property has to be treated as normal one
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def class_method(cls) -> Self: ...
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def static_method() -> Any: ...
|
||||||
|
|
||||||
|
def __private_method(self) -> Any: ...
|
||||||
|
|
||||||
|
def _protected_method(self) -> Any: ...
|
||||||
|
|
||||||
|
def usual_method(self) -> Any: ...
|
||||||
|
|
||||||
|
def __hash__(self) -> int: ... # basically any magic (or look-alike) method
|
||||||
|
```
|
||||||
|
|
||||||
|
Methods inside one group should be ordered alphabetically, the only exceptions are `__init__` (`__post_init__` for dataclasses), `__new__` and `__del__` methods which should be defined first. For test methods it is recommended to follow the order in which functions are defined. Same idea applies to frontend classes.
|
||||||
|
|
||||||
|
Though, we would like to highlight abstract methods (i.e. ones which raise `NotImplementedError`), we still keep in global order at the moment.
|
||||||
|
|
||||||
|
For the most cases there is custom `pylint` plugin, which performs checks automatically.
|
||||||
|
|
||||||
|
* Abstract methods must raise `NotImplementedError` instead of using `abc.abstractmethod`. The reason behind this restriction is the fact that we have class/static abstract methods for those we need to define their attribute first making the code harder to read.
|
||||||
|
* For any path interactions `pathlib.Path` must be used.
|
||||||
|
* Configuration interactions must go through `ahriman.core.configuration.Configuration` class instance.
|
||||||
|
* In case if class load requires some actions, it is recommended to create class method which can be used for class instantiating.
|
||||||
|
* The most (expected) exceptions must be handled and printed to log, allowing service to continue work. However, fatal and (in some cases) unexpected exceptions may lead to the application termination.
|
||||||
|
* Exceptions without parameters should be raised without parentheses, e.g.:
|
||||||
|
|
||||||
|
```python
|
||||||
|
raise RuntimeError
|
||||||
|
```
|
||||||
|
|
||||||
|
* For the external command `ahriman.core.util.check_output` function must be used.
|
||||||
|
* Every temporary file/directory must be removed at the end of processing, no matter what. The `tempfile` module provides good ways to do it.
|
||||||
|
* Import order must be the following:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# optional imports from future module
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# Module import for those which are installed into environment (no matter standard library or not)...
|
||||||
|
import os
|
||||||
|
# ...one per line...
|
||||||
|
import pyalpm
|
||||||
|
# ...in alphabetical order
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Blank line between
|
||||||
|
# ``from module import submodule`` import
|
||||||
|
from pathlib import Path
|
||||||
|
# ...again in alphabet order. It is possible to do several imports, but make sure that they are also in alphabetical order.
|
||||||
|
from pyalpm import Handle, Package
|
||||||
|
|
||||||
|
# Blank line again and package imports
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
# Multiline import example
|
||||||
|
from ahriman.core.database.operations import (
|
||||||
|
AuthOperations,
|
||||||
|
BuildOperations,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
* Imports goes in alphabetical order, no relative imports allowed. Same rule applies to frontend classes.
|
||||||
|
* One file should define only one class, exception is class satellites in case if file length remains less than 400 lines. Same rule applies to frontend classes.
|
||||||
|
* It is possible to create file which contains some functions (e.g. `ahriman.core.utils`), but in this case you would need to define `__all__` attribute.
|
||||||
|
* The file size mentioned above must be applicable in general. In case of big classes consider splitting them into traits. Note, however, that `pylint` includes comments and docstrings into counter, thus you need to check file size by other tools.
|
||||||
|
* No global variable is allowed outside of `ahriman` module. `ahriman.core.context` is also special case.
|
||||||
|
* Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent.
|
||||||
|
* If your class writes anything to log, the `ahriman.core.log.LazyLogging` trait must be used.
|
||||||
|
* Web API methods must be documented by using `aiohttp_apispec` library. The schema testing mostly should be implemented in related view class tests. Recommended example for documentation (excluding comments):
|
||||||
|
|
||||||
|
```python
|
||||||
|
from marshmallow import Schema, fields
|
||||||
|
|
||||||
|
from ahriman.web.apispec.decorators import apidocs
|
||||||
|
from ahriman.web.schemas import PackageNameSchema, PaginationSchema
|
||||||
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
class RequestSchema(Schema):
|
||||||
|
|
||||||
|
field = fields.String(metadata={"description": "Field description", "example": "foo"})
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseSchema(Schema):
|
||||||
|
|
||||||
|
field = fields.String(required=True, metadata={"description": "Field description"})
|
||||||
|
|
||||||
|
|
||||||
|
class Foo(BaseView):
|
||||||
|
|
||||||
|
POST_PERMISSION = ...
|
||||||
|
ROUTES = ...
|
||||||
|
|
||||||
|
@apidocs(
|
||||||
|
tags=["Tag"],
|
||||||
|
summary="Do foo",
|
||||||
|
description="Extended description of the method which does foo",
|
||||||
|
error_400_enabled=True, # exception raised by this method
|
||||||
|
error_404_description="Repository is unknown",
|
||||||
|
schema=ResponseSchema, # leave empty if no responses here
|
||||||
|
match_schema=PackageNameSchema,
|
||||||
|
query_schema=PaginationSchema,
|
||||||
|
body_schema=RequestSchema(many=True),
|
||||||
|
)
|
||||||
|
async def post(self) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
* It is allowed to change web API to add new fields or remove optional ones. However, in case of model changes, new API version must be introduced.
|
||||||
|
* On the other hand, it is allowed to change method signatures, however, it is recommended to add new parameters as optional if possible. Deprecated API can be dropped during major release.
|
||||||
|
* Enumerations (`Enum` classes) are allowed and recommended. However, it is recommended to use `StrEnum` class if there are from/to string conversions and `IntEnum` otherwise.
|
||||||
|
* `Generator` return type is not allowed. Generator functions must return generic `Iterator` object. Documentation should be described as `Yields`, however, because of pylint checks. Unfortunately, `Iterable` return type is not available for generators also, because of specific `contextlib.contextmanager` case.
|
||||||
|
|
||||||
|
### Other checks
|
||||||
|
|
||||||
|
The projects also uses typing checks (provided by `mypy`) and some linter checks provided by `pylint` and `bandit`. Those checks must be passed successfully for any open pull requests.
|
||||||
|
|
||||||
|
Frontend checks normally are performed by `eslint` (e.g. `npx run eslint`).
|
||||||
|
|
||||||
|
## Developers how to
|
||||||
|
|
||||||
|
### Run automated checks
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tox
|
||||||
|
```
|
||||||
|
|
||||||
|
Must be usually done before any pushes.
|
||||||
|
|
||||||
|
### Generate documentation templates
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tox -e docs
|
||||||
|
```
|
||||||
|
|
||||||
|
Must be usually done if there are changes in modules structure.
|
||||||
|
|
||||||
|
### Create release
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tox -m release -- major.minor.patch
|
||||||
|
```
|
||||||
|
|
||||||
|
The command above will generate documentation, tags, etc., and will push them to GitHub. Other things will be handled by GitHub workflows automatically.
|
||||||
|
|
||||||
|
### Hotfixes policy
|
||||||
|
|
||||||
|
Sometimes it is required to publish hotfix with specific commits, but some features have been already committed, which should not be included to the hotfix. In this case, some manual steps are required:
|
||||||
|
|
||||||
|
1. Create new branch from the last stable release (`major.minor.patch`):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git checkout -b release/major.minor major.minor.patch
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Cherry-pick desired commit(s):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git cherry-pick <commit-sha>
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, make changes to the new branch and commit them.
|
||||||
|
|
||||||
|
3. Push newly created branch to remote:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git push --set-upstream origin release/major.minor
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Proceed to release as usual:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tox -m release -- major.minor.patch+1
|
||||||
|
```
|
||||||
@@ -1,27 +1,44 @@
|
|||||||
# ArcHlinux ReposItory MANager
|
# ArcH linux ReposItory MANager
|
||||||
|
|
||||||
|
[](https://github.com/arcan1s/ahriman/actions/workflows/tests.yml)
|
||||||
|
[](https://github.com/arcan1s/ahriman/actions/workflows/setup.yml)
|
||||||
|
[](https://hub.docker.com/r/arcan1s/ahriman)
|
||||||
|
[](https://www.codefactor.io/repository/github/arcan1s/ahriman)
|
||||||
|
[](https://ahriman.readthedocs.io)
|
||||||
|
|
||||||
Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts).
|
Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Install-configure-forget manager for own repository
|
* Install-configure-forget manager for the very own repository.
|
||||||
* Multi-architecture support
|
* Multi architecture and repository support.
|
||||||
* VCS packages support
|
* Dependency manager.
|
||||||
* Sign support with gpg (repository, package, per package settings)
|
* VCS packages support.
|
||||||
* Synchronization to remote services (rsync, s3) and report generation (html)
|
* Official repository support.
|
||||||
* Dependency manager
|
* Ability to patch AUR packages and even create package from local PKGBUILDs.
|
||||||
* Repository status interface
|
* Various rebuild options with ability to automatically bump package version.
|
||||||
|
* Sign support with gpg (repository, package), multiple packagers support.
|
||||||
|
* Triggers for repository updates, e.g. synchronization to remote services (rsync, S3 and GitHub) and report generation (email, html, telegram).
|
||||||
|
* Repository status interface with optional authorization and control options:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Installation and run
|
## Installation and run
|
||||||
|
|
||||||
* Install package as usual.
|
For installation details kindly refer to the [documentation](https://ahriman.readthedocs.io/en/stable/setup.html). For application commands it is possible to get information by using `--help`/`help` command or by using man page ([web version](https://ahriman.readthedocs.io/en/stable/command-line.html)).
|
||||||
* Change settings if required, see `CONFIGURING.md` for more details.
|
|
||||||
* Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`).
|
## Configuration
|
||||||
* Configure build tools (it might be required if your package will use any custom repositories):
|
|
||||||
* create build command, e.g. `ln -s /usr/bin/archbuild /usr/local/bin/custom-x86_64-build` (you can choose any name for command);
|
Every available option is described in the [documentation](https://ahriman.readthedocs.io/en/stable/configuration.html).
|
||||||
* create configuration file, e.g. `cp /usr/share/devtools/pacman-{extra,custom}.conf`;
|
|
||||||
* change configuration file, add your own repository, add multilib repository etc;
|
The application provides reasonable defaults which allow to use it out-of-box; however additional steps (like configuring build toolchain and sudoers) are recommended and can be easily achieved by following install instructions.
|
||||||
* set `build.build_command` setting to point to your command;
|
|
||||||
* configure `/etc/sudoers.d/ahriman` to allow running command without password.
|
## [FAQ](https://ahriman.readthedocs.io/en/stable/faq/index.html)
|
||||||
* Start and enable `ahriman.timer` via `systemctl`.
|
|
||||||
* Add packages by using `ahriman add {package}` command.
|
## Live demos
|
||||||
|
|
||||||
|
* [Build status page](https://ahriman-demo.arcanis.me). You can log in as `demo` user by using `demo` password. However, you will not be able to run tasks. [HTTP API documentation](https://ahriman-demo.arcanis.me/api-docs) is also available.
|
||||||
|
* [Repository index](https://repo.arcanis.me/arcanisrepo/x86_64/).
|
||||||
|
* [Telegram feed](https://t.me/arcanisrepo).
|
||||||
|
|
||||||
|
Do you have any success story? You can [share it](https://github.com/arcan1s/ahriman/issues/new?template=04-discussion.md)!
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
The project follows bleeding edge philosophy, thus only the latest version is supported with the exception for release candidates (i.e. tags which are marked with `rc` suffix).
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
In the most cases you can report (suspected) security vulnerabilities directly on github by using ["Security report" template](https://github.com/arcan1s/ahriman/issues/new?assignees=&labels=security&template=02-security-report.md&title=). However, if your report could lead to data leak or break the system we kindly ask you to contact [current maintainer](AUTHORS) directly by email.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=ArcH linux ReposItory MANager (%i)
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/ahriman --repository-id "%I" repo-daemon --refresh
|
||||||
|
User=ahriman
|
||||||
|
Group=ahriman
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=ArcH linux ReposItory MANager (%i)
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/ahriman --repository-id "%I" repo-update --refresh
|
||||||
|
User=ahriman
|
||||||
|
Group=ahriman
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=ArcH linux ReposItory MANager timer (%i)
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=daily
|
||||||
|
RandomizedDelaySec=3600
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
[settings]
|
||||||
|
; Relative path to directory with configuration files overrides. Overrides will be applied in alphabetic order.
|
||||||
|
include = ahriman.ini.d
|
||||||
|
; Relative path to configuration used by logging package.
|
||||||
|
logging = ahriman.ini.d/logging.ini
|
||||||
|
; Perform database migrations on the application start. Do not touch this option unless you know what you are doing.
|
||||||
|
;apply_migrations = yes
|
||||||
|
; Path to the application SQLite database.
|
||||||
|
database = ${repository:root}/ahriman.db
|
||||||
|
|
||||||
|
[alpm]
|
||||||
|
; Path to pacman system database cache.
|
||||||
|
database = /var/lib/pacman
|
||||||
|
; Arch linux mirror used by local pacman for synchronization.
|
||||||
|
mirror = https://geo.mirror.pkgbuild.com/$repo/os/$arch
|
||||||
|
; Space separated list of pacman repositories to search for packages.
|
||||||
|
repositories = core extra multilib
|
||||||
|
; Pacman's root directory. In the most cases it must point to the system root.
|
||||||
|
root = /
|
||||||
|
; Sync files databases too, which is required by deep dependencies check.
|
||||||
|
sync_files_database = yes
|
||||||
|
; Use local packages cache. If this option is enabled, the service will be able to synchronize databases (available
|
||||||
|
; as additional option for some subcommands). If set to no, databases must be synchronized manually.
|
||||||
|
use_ahriman_cache = yes
|
||||||
|
|
||||||
|
[aur]
|
||||||
|
; Maximum amount of retries of HTTP requests.
|
||||||
|
max_retries = 3
|
||||||
|
; Retry exponential backoff.
|
||||||
|
retry_backoff = 1.0
|
||||||
|
; HTTP request timeout in seconds.
|
||||||
|
;timeout = 30
|
||||||
|
|
||||||
|
[build]
|
||||||
|
; List of additional flags passed to archbuild command.
|
||||||
|
;archbuild_flags =
|
||||||
|
; Path to build command.
|
||||||
|
;build_command =
|
||||||
|
; List of packages to be ignored during automatic updates.
|
||||||
|
;ignore_packages =
|
||||||
|
; Include debug packages.
|
||||||
|
;include_debug_packages = yes
|
||||||
|
; List of additional flags passed to makechrootpkg command.
|
||||||
|
;makechrootpkg_flags =
|
||||||
|
; List of additional flags passed to makepkg command.
|
||||||
|
makepkg_flags = --nocolor --ignorearch
|
||||||
|
; List of paths to be used for implicit dependency scan. Regular expressions are supported.
|
||||||
|
scan_paths = ^usr/lib(?!/cmake).*$
|
||||||
|
; List of enabled triggers in the order of calls.
|
||||||
|
triggers[] = ahriman.core.gitremote.RemotePullTrigger
|
||||||
|
triggers[] = ahriman.core.report.ReportTrigger
|
||||||
|
triggers[] = ahriman.core.upload.UploadTrigger
|
||||||
|
triggers[] = ahriman.core.gitremote.RemotePushTrigger
|
||||||
|
triggers[] = ahriman.core.housekeeping.LogsRotationTrigger
|
||||||
|
triggers[] = ahriman.core.housekeeping.ArchiveRotationTrigger
|
||||||
|
; List of well-known triggers. Used only for configuration purposes.
|
||||||
|
triggers_known[] = ahriman.core.gitremote.RemotePullTrigger
|
||||||
|
triggers_known[] = ahriman.core.gitremote.RemotePushTrigger
|
||||||
|
triggers_known[] = ahriman.core.housekeeping.ArchiveRotationTrigger
|
||||||
|
triggers_known[] = ahriman.core.housekeeping.LogsRotationTrigger
|
||||||
|
triggers_known[] = ahriman.core.report.ReportTrigger
|
||||||
|
triggers_known[] = ahriman.core.upload.UploadTrigger
|
||||||
|
; Maximal age in seconds of the VCS packages before their version will be updated with its remote source.
|
||||||
|
;vcs_allowed_age = 604800
|
||||||
|
|
||||||
|
[repository]
|
||||||
|
; Application root.
|
||||||
|
root = /var/lib/ahriman
|
||||||
|
|
||||||
|
[sign]
|
||||||
|
; Enable repository or package signing. Must be one of repository, package.
|
||||||
|
target =
|
||||||
|
; PGP key used for signing as default.
|
||||||
|
;key =
|
||||||
|
|
||||||
|
[status]
|
||||||
|
; Global switch to enable or disable status reporting.
|
||||||
|
enabled = yes
|
||||||
|
; Address of the remote service, e.g.:
|
||||||
|
; address = http://127.0.0.1:8080
|
||||||
|
; In case if unix sockets are used, it might point to the valid socket with encoded path, e.g.:
|
||||||
|
; address = http+unix://%2Fvar%2Flib%2Fahriman%2Fsocket
|
||||||
|
;address = http://${web:host}:${web:port}
|
||||||
|
; Maximum amount of retries of HTTP requests.
|
||||||
|
;max_retries = 0
|
||||||
|
; Optional password for authentication (if enabled).
|
||||||
|
;password =
|
||||||
|
; Retry exponential backoff.
|
||||||
|
;retry_backoff = 0.0
|
||||||
|
; Do not log HTTP errors if occurs.
|
||||||
|
suppress_http_log_errors = yes
|
||||||
|
; HTTP request timeout in seconds.
|
||||||
|
;timeout = 30
|
||||||
|
; Optional username for authentication (if enabled).
|
||||||
|
;username =
|
||||||
|
|
||||||
|
[remote-pull]
|
||||||
|
; List of configuration section names for git remote pull plugin, e.g.:
|
||||||
|
; target = remote-pull-trigger
|
||||||
|
target =
|
||||||
|
|
||||||
|
; git remote pull trigger sample.
|
||||||
|
;[remote-pull-trigger]
|
||||||
|
; Valid URL to pull repository, e.g.:
|
||||||
|
; pull_url = https://github.com/arcan1s/arcanisrepo.git
|
||||||
|
;pull_url =
|
||||||
|
; Remote branch to pull.
|
||||||
|
;pull_branch = master
|
||||||
|
|
||||||
|
[remote-push]
|
||||||
|
; List of configuration section names for git remote push plugin, e.g.:
|
||||||
|
; target = remote-push-trigger
|
||||||
|
target =
|
||||||
|
|
||||||
|
; git remote push trigger sample.
|
||||||
|
;[remote-push-trigger]
|
||||||
|
; Author commit email.
|
||||||
|
;commit_email = ahriman@localhost
|
||||||
|
; Author commit user.
|
||||||
|
;commit_user = ahriman
|
||||||
|
; Valid URL to push repository, e.g.:
|
||||||
|
; push_url = https://key:token@github.com/arcan1s/arcanisrepo.git
|
||||||
|
; Note, that more likely authentication must be enabled.
|
||||||
|
;push_url =
|
||||||
|
; Remote branch to push.
|
||||||
|
;push_branch = master
|
||||||
|
|
||||||
|
[report]
|
||||||
|
; List of configuration section names for reporting plugin.
|
||||||
|
target = console
|
||||||
|
|
||||||
|
; Console reporting trigger configuration sample.
|
||||||
|
[console]
|
||||||
|
; Trigger type name.
|
||||||
|
;type = console
|
||||||
|
; Use utf8 symbols in output.
|
||||||
|
use_utf = yes
|
||||||
|
|
||||||
|
; Email reporting trigger configuration sample.
|
||||||
|
[email]
|
||||||
|
; Trigger type name.
|
||||||
|
;type = email
|
||||||
|
; Optional URL to the repository homepage.
|
||||||
|
;homepage=
|
||||||
|
; SMTP server address.
|
||||||
|
;host =
|
||||||
|
; Prefix for packages links. Link to a package will be formed as link_path / filename.
|
||||||
|
;link_path =
|
||||||
|
; Skip report generation if no packages were updated.
|
||||||
|
;no_empty_report = yes
|
||||||
|
; SMTP password.
|
||||||
|
;password =
|
||||||
|
; SMTP server port.
|
||||||
|
;port =
|
||||||
|
; List of emails to receive the reports.
|
||||||
|
;receivers =
|
||||||
|
; Optional link to the RSS feed.
|
||||||
|
;rss_url =
|
||||||
|
; Sender email.
|
||||||
|
;sender =
|
||||||
|
; SMTP server SSL mode, one of ssl, starttls, disabled.
|
||||||
|
;ssl = disabled
|
||||||
|
; Template name to be used.
|
||||||
|
template = email-index.jinja2
|
||||||
|
; Template name to be used for full packages list generation (same as HTML report).
|
||||||
|
;template_full =
|
||||||
|
; List of directories with templates.
|
||||||
|
templates[] = ${prefix}/share/ahriman/templates
|
||||||
|
; SMTP user.
|
||||||
|
;user =
|
||||||
|
|
||||||
|
; HTML reporting trigger configuration sample.
|
||||||
|
[html]
|
||||||
|
; Trigger type name.
|
||||||
|
;type = html
|
||||||
|
; Optional URL to the repository homepage.
|
||||||
|
;homepage=
|
||||||
|
; Prefix for packages links. Link to a package will be formed as link_path / filename.
|
||||||
|
;link_path =
|
||||||
|
; Output path for the HTML report.
|
||||||
|
;path =
|
||||||
|
; Optional link to the RSS feed.
|
||||||
|
;rss_url =
|
||||||
|
; Template name to be used.
|
||||||
|
template = repo-index.jinja2
|
||||||
|
; List of directories with templates.
|
||||||
|
templates[] = ${prefix}/share/ahriman/templates
|
||||||
|
|
||||||
|
; Remote service callback trigger configuration sample.
|
||||||
|
[remote-call]
|
||||||
|
; Trigger type name.
|
||||||
|
;type = remote-call
|
||||||
|
; Call for AUR packages update.
|
||||||
|
;aur = no
|
||||||
|
; Call for local packages update.
|
||||||
|
;local = no
|
||||||
|
; Call for manual packages update.
|
||||||
|
;manual = no
|
||||||
|
; Wait until remote process will be terminated in seconds.
|
||||||
|
;wait_timeout = -1
|
||||||
|
|
||||||
|
; RSS reporting trigger configuration sample.
|
||||||
|
[rss]
|
||||||
|
; Trigger type name.
|
||||||
|
;type = rss
|
||||||
|
; Optional URL to the repository homepage.
|
||||||
|
;homepage=
|
||||||
|
; Prefix for packages links. Link to a package will be formed as link_path / filename.
|
||||||
|
;link_path =
|
||||||
|
; Output path for the RSS report.
|
||||||
|
;path =
|
||||||
|
; Optional link to the RSS feed.
|
||||||
|
;rss_url =
|
||||||
|
; Template name to be used.
|
||||||
|
template = rss.jinja2
|
||||||
|
; List of directories with templates.
|
||||||
|
templates[] = ${prefix}/share/ahriman/templates
|
||||||
|
|
||||||
|
; Telegram reporting trigger configuration sample.
|
||||||
|
[telegram]
|
||||||
|
; Trigger type name.
|
||||||
|
;type = telegram
|
||||||
|
; Telegram bot API key.
|
||||||
|
;api_key =
|
||||||
|
; Telegram chat ID.
|
||||||
|
;chat_id =
|
||||||
|
; Optional URL to the repository homepage.
|
||||||
|
;homepage=
|
||||||
|
; Prefix for packages links. Link to a package will be formed as link_path / filename.
|
||||||
|
;link_path =
|
||||||
|
; Maximum amount of retries of HTTP requests.
|
||||||
|
;max_retries = 0
|
||||||
|
; Retry exponential backoff.
|
||||||
|
;retry_backoff = 0.0
|
||||||
|
; Optional link to the RSS feed.
|
||||||
|
;rss_url =
|
||||||
|
; Template name to be used.
|
||||||
|
template = telegram-index.jinja2
|
||||||
|
; Telegram specific template mode, one of MarkdownV2, HTML or Markdown.
|
||||||
|
;template_type = HTML
|
||||||
|
; List of directories with templates.
|
||||||
|
templates[] = ${prefix}/share/ahriman/templates
|
||||||
|
; HTTP request timeout in seconds.
|
||||||
|
;timeout = 30
|
||||||
|
|
||||||
|
[upload]
|
||||||
|
; List of configuration section names for remote upload plugin, e.g.:
|
||||||
|
; target = rsync s3
|
||||||
|
target =
|
||||||
|
|
||||||
|
; GitHub upload trigger configuration sample.
|
||||||
|
[github]
|
||||||
|
; Trigger type name.
|
||||||
|
;type = github
|
||||||
|
; Maximum amount of retries of HTTP requests.
|
||||||
|
;max_retries = 0
|
||||||
|
; GitHub repository owner username.
|
||||||
|
;owner =
|
||||||
|
; GitHub API key. public_repo (repo) scope is required.
|
||||||
|
;password =
|
||||||
|
; GitHub repository name.
|
||||||
|
;repository =
|
||||||
|
; Retry exponential backoff.
|
||||||
|
;retry_backoff = 0.0
|
||||||
|
; HTTP request timeout in seconds.
|
||||||
|
;timeout = 30
|
||||||
|
; Include repository name to release name (recommended).
|
||||||
|
;use_full_release_name = no
|
||||||
|
; GitHub authentication username.
|
||||||
|
;username =
|
||||||
|
|
||||||
|
; Remote instance upload trigger configuration sample.
|
||||||
|
[remote-service]
|
||||||
|
; Trigger type name.
|
||||||
|
;type = remote-service
|
||||||
|
; Maximum amount of retries of HTTP requests.
|
||||||
|
;max_retries = 0
|
||||||
|
; Retry exponential backoff.
|
||||||
|
;retry_backoff = 0.0
|
||||||
|
; HTTP request timeout in seconds.
|
||||||
|
;timeout = 30
|
||||||
|
|
||||||
|
; rsync upload trigger configuration sample.
|
||||||
|
[rsync]
|
||||||
|
; Trigger type name.
|
||||||
|
;type = rsync
|
||||||
|
; rsync command to run.
|
||||||
|
command = rsync --archive --compress --partial --delete
|
||||||
|
; Remote address and directory to sync, e.g.:
|
||||||
|
; remote = ahriman@10.0.0.1:/srv/repo
|
||||||
|
;remote =
|
||||||
|
|
||||||
|
; S3 upload trigger configuration sample.
|
||||||
|
[s3]
|
||||||
|
; Trigger type name.
|
||||||
|
;type = s3
|
||||||
|
; AWS services access key.
|
||||||
|
;access_key =
|
||||||
|
; AWS S3 bucket name.
|
||||||
|
;bucket =
|
||||||
|
; Chunk size tp calculate ETags. Do not edit this value.
|
||||||
|
;chunk_size = 8388608
|
||||||
|
; Optional path prefix for stored objects.
|
||||||
|
;object_path =
|
||||||
|
; AWS S3 bucket region.
|
||||||
|
;region =
|
||||||
|
; AWS services secret key.
|
||||||
|
;secret_key =
|
||||||
|
|
||||||
|
; Remote worker configuration sample.
|
||||||
|
;[worker]
|
||||||
|
; Remotely reachable address of this instance, e.g.:
|
||||||
|
; address = http://10.0.0.1:8080
|
||||||
|
;address =
|
||||||
|
; Unique identifier of this instance if any.
|
||||||
|
;identifier =
|
||||||
|
; Maximum amount of time in seconds after which worker will be considered offline in case of no reports.
|
||||||
|
;time_to_live = 60
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[archive]
|
||||||
|
; Keep amount of last built packages in archive. 0 means keep all packages
|
||||||
|
keep_built_packages = 1
|
||||||
|
|
||||||
|
[logs-rotation]
|
||||||
|
; Keep last build logs for each package
|
||||||
|
keep_last_logs = 5
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
[loggers]
|
||||||
|
keys = root,http,stderr,boto3,botocore,nose,s3transfer,sql
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console_handler,journald_handler,syslog_handler
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic_format,syslog_format
|
||||||
|
|
||||||
|
[handler_console_handler]
|
||||||
|
class = logging.StreamHandler
|
||||||
|
level = DEBUG
|
||||||
|
formatter = generic_format
|
||||||
|
args = (sys.stderr,)
|
||||||
|
|
||||||
|
[handler_journald_handler]
|
||||||
|
class = ahriman.core.log.journal_handler.JournalHandler
|
||||||
|
level = DEBUG
|
||||||
|
formatter = syslog_format
|
||||||
|
kwargs = {"SYSLOG_IDENTIFIER": "ahriman"}
|
||||||
|
|
||||||
|
[handler_syslog_handler]
|
||||||
|
class = logging.handlers.SysLogHandler
|
||||||
|
level = DEBUG
|
||||||
|
formatter = syslog_format
|
||||||
|
args = ("/dev/log",)
|
||||||
|
|
||||||
|
[formatter_generic_format]
|
||||||
|
format = [{levelname} {asctime}] [{name}]: {message}
|
||||||
|
style = {
|
||||||
|
|
||||||
|
[formatter_syslog_format]
|
||||||
|
format = [{levelname}] [{name}]: {message}
|
||||||
|
style = {
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = DEBUG
|
||||||
|
qualname = root
|
||||||
|
|
||||||
|
[logger_http]
|
||||||
|
level = WARNING
|
||||||
|
qualname = http
|
||||||
|
propagate = 0
|
||||||
|
|
||||||
|
[logger_stderr]
|
||||||
|
level = DEBUG
|
||||||
|
handlers = console_handler
|
||||||
|
qualname = stderr
|
||||||
|
|
||||||
|
[logger_boto3]
|
||||||
|
level = INFO
|
||||||
|
qualname = boto3
|
||||||
|
propagate = 0
|
||||||
|
|
||||||
|
[logger_botocore]
|
||||||
|
level = INFO
|
||||||
|
qualname = botocore
|
||||||
|
propagate = 0
|
||||||
|
|
||||||
|
[logger_nose]
|
||||||
|
level = INFO
|
||||||
|
qualname = nose
|
||||||
|
propagate = 0
|
||||||
|
|
||||||
|
[logger_s3transfer]
|
||||||
|
level = INFO
|
||||||
|
qualname = s3transfer
|
||||||
|
propagate = 0
|
||||||
|
|
||||||
|
[logger_sql]
|
||||||
|
level = INFO
|
||||||
|
qualname = sql
|
||||||
|
propagate = 0
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{#simplified version of full report#}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{{ repository }}</title>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
{% include "utils/style.jinja2" %}
|
||||||
|
{% include "user-style.jinja2" ignore missing %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<table id="packages" class="table table-striped">
|
||||||
|
<thead class="table-primary">
|
||||||
|
<tr>
|
||||||
|
<th>package</th>
|
||||||
|
<th>version</th>
|
||||||
|
<th>archive size</th>
|
||||||
|
<th>installed size</th>
|
||||||
|
<th>build date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{% for package in packages %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ link_path }}/{{ package.filename }}" title="{{ package.name }}">{{ package.name }}</a></td>
|
||||||
|
<td>{{ package.version }}</td>
|
||||||
|
<td>{{ package.archive_size }}</td>
|
||||||
|
<td>{{ package.installed_size }}</td>
|
||||||
|
<td>{{ package.build_date }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{{ repository }}</title>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
{% include "utils/style.jinja2" %}
|
||||||
|
{% include "user-style.jinja2" ignore missing %}
|
||||||
|
|
||||||
|
{% if rss_url is not none %}
|
||||||
|
<link rel="alternate" href="{{ rss_url }}" type="application/rss+xml">
|
||||||
|
{% endif %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
{% include "utils/bootstrap-scripts.jinja2" %}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1>Arch linux user repository</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{% if pgp_key is not none %}
|
||||||
|
<p>This repository is signed with <a href="https://keyserver.ubuntu.com/pks/lookup?search=0x{{ pgp_key }}&fingerprint=on&op=index" title="key search">{{ pgp_key }}</a> by default.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>In order to use this repository edit your <code>/etc/pacman.conf</code> as following:</p>
|
||||||
|
|
||||||
|
<pre class="language-ini"><code id="pacman-conf" class="language-ini">[{{ repository }}]
|
||||||
|
Server = {{ link_path }}
|
||||||
|
SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly</code><button id="copy-btn" type="button" class="btn language-ini" onclick="copyPacmanConf()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<table id="packages"
|
||||||
|
data-classes="table table-hover"
|
||||||
|
data-export-options='{"fileName": "packages"}'
|
||||||
|
data-filter-control="true"
|
||||||
|
data-filter-control-visible="false"
|
||||||
|
data-page-list="[10, 25, 50, 100, all]"
|
||||||
|
data-page-size="10"
|
||||||
|
data-pagination="true"
|
||||||
|
data-resizable="true"
|
||||||
|
data-search="true"
|
||||||
|
data-show-columns="true"
|
||||||
|
data-show-columns-search="true"
|
||||||
|
data-show-columns-toggle-all="true"
|
||||||
|
data-show-export="true"
|
||||||
|
data-show-filter-control-switch="true"
|
||||||
|
data-show-fullscreen="true"
|
||||||
|
data-show-search-clear-button="true"
|
||||||
|
data-sortable="true"
|
||||||
|
data-sort-name="base"
|
||||||
|
data-sort-order="asc">
|
||||||
|
<thead class="table-primary">
|
||||||
|
<tr>
|
||||||
|
<th data-sortable="true" data-switchable="false" data-field="name" data-filter-control="input" data-filter-control-placeholder="(any package)">package</th>
|
||||||
|
<th data-sortable="true" data-align="right" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th>
|
||||||
|
<th data-sortable="true" data-visible="false" data-field="architecture" data-filter-control="select" data-filter-control-placeholder="(any arch)">architecture</th>
|
||||||
|
<th data-sortable="true" data-visible="false" data-field="description" data-filter-control="input" data-filter-control-placeholder="(any description)">description</th>
|
||||||
|
<th data-sortable="true" data-visible="false" data-field="url">upstream url</th>
|
||||||
|
<th data-sortable="true" data-visible="false" data-field="licenses" data-filter-control="select" data-filter-data="func:filterListLicenses" data-filter-custom-search="filterList" data-filter-control-placeholder="(any license)">licenses</th>
|
||||||
|
<th data-sortable="true" data-visible="false" data-field="groups" data-filter-control="select" data-filter-data="func:filterListGroups" data-filter-custom-search="filterList" data-filter-control-placeholder="(any group)">groups</th>
|
||||||
|
<th data-sortable="true" data-visible="false" data-field="depends" data-filter-control="select" data-filter-data="func:filterListDepends" data-filter-custom-search="filterList" data-filter-control-placeholder="(any depends)">depends</th>
|
||||||
|
<th data-sortable="true" data-align="right" data-field="archive_size">archive size</th>
|
||||||
|
<th data-sortable="true" data-align="right" data-field="installed_size">installed size</th>
|
||||||
|
<th data-sortable="true" data-align="right" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">build date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{% for package in packages %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ link_path }}/{{ package.filename }}" title="{{ package.name }}">{{ package.name }}</a></td>
|
||||||
|
<td>{{ package.version }}</td>
|
||||||
|
<td>{{ package.architecture }}</td>
|
||||||
|
<td>{{ package.description }}</td>
|
||||||
|
<td><a href="{{ package.url }}" title="{{ package.name }} upstream url">{{ package.url }}</a></td>
|
||||||
|
<td>{{ package.licenses | join("<br>" | safe) }}</td>
|
||||||
|
<td>{{ package.groups | join("<br>" | safe) }}</td>
|
||||||
|
<td>{{ package.depends | join("<br>" | safe) }}</td>
|
||||||
|
<td>{{ package.archive_size }}</td>
|
||||||
|
<td>{{ package.installed_size }}</td>
|
||||||
|
<td>{{ package.build_date }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
|
||||||
|
<ul class="nav">
|
||||||
|
{% if homepage is not none %}
|
||||||
|
<li><a class="nav-link" href="{{ homepage }}" title="homepage"><i class="bi bi-house"></i> homepage</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
<ul class="nav">
|
||||||
|
<li><a class="nav-link" href="https://github.com/arcan1s/ahriman" title="sources"><i class="bi bi-github"></i> ahriman</a></li>
|
||||||
|
</ul>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const table = $(document.getElementById("packages"));
|
||||||
|
|
||||||
|
const pacmanConf = document.getElementById("pacman-conf");
|
||||||
|
const pacmanConfCopyButton = document.getElementById("copy-btn");
|
||||||
|
|
||||||
|
async function copyPacmanConf() {
|
||||||
|
const conf = pacmanConf.textContent;
|
||||||
|
await copyToClipboard(conf, pacmanConfCopyButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterListDepends() {
|
||||||
|
return extractDataList(table.bootstrapTable("getData"), "depends");
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterListGroups() {
|
||||||
|
return extractDataList(table.bootstrapTable("getData"), "groups");
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterListLicenses() {
|
||||||
|
return extractDataList(table.bootstrapTable("getData"), "licenses");
|
||||||
|
}
|
||||||
|
|
||||||
|
ready(_ => {
|
||||||
|
table.bootstrapTable({
|
||||||
|
onCreatedControls: _ => {
|
||||||
|
new easepick.create({
|
||||||
|
element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
|
||||||
|
css: [
|
||||||
|
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
|
||||||
|
],
|
||||||
|
grid: 2,
|
||||||
|
calendars: 2,
|
||||||
|
autoApply: false,
|
||||||
|
locale: {
|
||||||
|
cancel: "Clear",
|
||||||
|
},
|
||||||
|
RangePlugin: {
|
||||||
|
tooltip: false,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
"RangePlugin",
|
||||||
|
],
|
||||||
|
setup: picker => {
|
||||||
|
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
|
||||||
|
// replace "Cancel" behaviour to "Clear"
|
||||||
|
picker.onClickCancelButton = element => {
|
||||||
|
if (picker.isCancelButton(element)) {
|
||||||
|
picker.clear();
|
||||||
|
picker.hide();
|
||||||
|
table.bootstrapTable("triggerSearch");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<channel>
|
||||||
|
<title>{{ repository }}: Recent package updates</title>
|
||||||
|
{% if homepage is not none %}
|
||||||
|
<link>{{ homepage }}</link>
|
||||||
|
{% endif %}
|
||||||
|
<description>Recently updated packages in the {{ repository }}.</description>
|
||||||
|
{% if rss_url is not none %}
|
||||||
|
<atom:link href="{{ rss_url }}" rel="self"/>
|
||||||
|
{% endif %}
|
||||||
|
<language>en-us</language>
|
||||||
|
<lastBuildDate>{{ last_update }}</lastBuildDate>
|
||||||
|
|
||||||
|
{% for package in packages %}
|
||||||
|
<item>
|
||||||
|
<title>{{ package.name }} {{ package.version }} {{ package.architecture }}</title>
|
||||||
|
<link>{{ link_path }}/{{ package.filename }}</link>
|
||||||
|
<description>{{ package.description }}</description>
|
||||||
|
<pubDate>{{ package.build_date }}</pubDate>
|
||||||
|
<guid isPermaLink="false">{{ package.tag }}</guid>
|
||||||
|
<category>{{ repository }}</category>
|
||||||
|
<category>{{ package.architecture }}</category>
|
||||||
|
</item>
|
||||||
|
{% endfor %}
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[00m
|
||||||
|
[38;5;60m▄[48;5;60;38;5;67m▄[49;38;5;60m▄[39m [38;5;60m▄▄▄▄▄▄[48;5;60m█[49m▀[39m [00m
|
||||||
|
[38;5;60m▄[48;5;60;38;5;67m▄[48;5;67;38;5;60m▄[48;5;60;38;5;103m▄[48;5;103;38;5;110m▄[48;5;60;38;5;103m▄[38;5;67m▄▄▄[48;5;67m██[38;5;60m▄▄▄[38;5;67m█[48;5;60m▄[49;38;5;60m▄[39m [00m
|
||||||
|
[48;5;60;38;5;60m█[48;5;67;38;5;67m█[48;5;60;38;5;103m▄[48;5;103;38;5;110m▄[48;5;110m██[48;5;103;38;5;103m█[48;5;60;38;5;67m▄▄▄▄[48;5;67;38;5;60m▄[38;5;67m██[48;5;60m▄[48;5;67;38;5;60m▄[38;5;67m█[48;5;60m▄[49;38;5;60m▄[39m [00m
|
||||||
|
[48;5;60;38;5;60m█[48;5;67m▄[48;5;103;38;5;103m█[48;5;110;38;5;110m█[48;5;103;38;5;103m█[48;5;110;38;5;110m██[48;5;103;38;5;103m█[48;5;60;38;5;110m▄▄[38;5;103m▄[48;5;110;38;5;110m█[48;5;60;38;5;60m█[48;5;67;38;5;67m██[48;5;60m▄[48;5;67;38;5;60m▄[38;5;67m█[48;5;60m▄[49;38;5;60m▄[39m [38;5;60m▄[39m[00m
|
||||||
|
[48;5;60;38;5;60m█[38;5;67m▄[48;5;103;38;5;60m▄[48;5;110;38;5;103m▄[38;5;110m█████████[48;5;60m▄▄[48;5;67;38;5;60m▄▄[49m▀[48;5;67m▄[38;5;67m█[48;5;60m▄[38;5;60m█[49m▀[39m[00m
|
||||||
|
[48;5;60;38;5;60m█[49m▄▀[48;5;67m▄[48;5;103;38;5;103m█[48;5;110;38;5;110m███[38;5;232m▄[38;5;110m█[38;5;232m▄▄▄▄[38;5;110m███[48;5;103m▄[49;38;5;103m▄[39m [38;5;60m▀▀[39m [00m
|
||||||
|
[48;5;60;38;5;60m█[48;5;67m▄[48;5;60;38;5;67m▄▄[48;5;103;38;5;103m█[48;5;110;38;5;110m███[38;5;232m▄[48;5;232m█[48;5;188;38;5;188m█[48;5;231;38;5;231m██[48;5;232;38;5;232m██[48;5;110;38;5;110m███[48;5;103;38;5;103m█[49;39m [00m
|
||||||
|
[38;5;60m▄▄[48;5;60;38;5;67m▄▄▄▄[49;38;5;60m▄▄▄[39m [38;5;60m▀[48;5;60m█[38;5;67m▄[48;5;103;38;5;103m█[48;5;110;38;5;110m████[48;5;232m▄[48;5;145;38;5;231m▄[48;5;232;38;5;145m▄[38;5;231m▄[38;5;232m█[38;5;231m▄[48;5;110;38;5;110m██[48;5;103;38;5;103m█[49m▄[39m [00m
|
||||||
|
[38;5;60m▄[48;5;60;38;5;67m▄[48;5;67m███[38;5;60m▄▄▄▄▄▄[48;5;60;38;5;67m▄▄[49;38;5;60m▄[39m [38;5;60m▀[39m [38;5;103m▀[48;5;110m▄[38;5;110m█████[48;5;188m▄▄[48;5;110m█[38;5;103m▄[38;5;110m██[48;5;103;38;5;103m█[49;39m [00m
|
||||||
|
[48;5;60;38;5;60m█[48;5;67;38;5;67m██[38;5;60m▄[48;5;60;38;5;67m▄[48;5;67m████[38;5;60m▄▄[48;5;60m█[48;5;67m▄[38;5;67m█[48;5;60;38;5;60m█[49;38;5;103m▄▄▄[39m [48;5;103;38;5;103m█[48;5;110;38;5;110m██[48;5;103m▄[48;5;110;38;5;103m▄▄▄▄▄[48;5;103m█[49m▀▀[39m [00m
|
||||||
|
[48;5;60;38;5;60m█[48;5;67;38;5;67m█[48;5;60;38;5;60m█[48;5;67;38;5;67m████[38;5;60m▄[49m▀[39m [48;5;60;38;5;103m▄[48;5;103;38;5;110m▄[48;5;110;38;5;179m▄[38;5;110m██[48;5;103m▄▄▄[48;5;110m████[48;5;103;38;5;103m█[49;39m [00m
|
||||||
|
[48;5;60;38;5;60m█[48;5;67;38;5;67m█[48;5;60m▄[48;5;67;38;5;60m▄[38;5;67m███[48;5;60;38;5;60m█[49;39m [48;5;103;38;5;103m█[48;5;185;38;5;110m▄[48;5;110m█[38;5;179m▄[48;5;179;38;5;110m▄[48;5;110m████████[48;5;103;38;5;103m█[49;39m [00m
|
||||||
|
[38;5;60m▄[48;5;60;38;5;67m▄[48;5;67m█[38;5;60m▄[48;5;60;38;5;67m▄[48;5;67m██[38;5;60m▄[49m▀[39m [38;5;103m▀[48;5;110m▄[38;5;110m████[38;5;103m▄[38;5;110m██[38;5;103m▄[38;5;110m██[38;5;103m▄[48;5;103;38;5;67m▄[49;39m [00m
|
||||||
|
[48;5;60;38;5;60m█[48;5;67;38;5;67m██[48;5;60;38;5;60m█[48;5;67;38;5;67m███[48;5;60;38;5;60m█[49;39m [38;5;103m▄[48;5;103m█[48;5;110;38;5;110m██[38;5;103m▄[48;5;103;38;5;110m▄[38;5;67m▄[38;5;103m█[38;5;110m▄[48;5;110m█[48;5;103;38;5;103m█[48;5;110;38;5;110m█[48;5;67;38;5;67m█[49;39m [00m
|
||||||
|
[38;5;60m▄[48;5;60;38;5;67m▄[48;5;67;38;5;60m▄[48;5;60;38;5;67m▄[48;5;67m███[38;5;60m▄[49m▀[39m [38;5;103m▄[48;5;103;38;5;110m▄[48;5;110m██[48;5;103;38;5;103m█[48;5;110;38;5;110m█[48;5;67;38;5;67m█[49;39m [48;5;103;38;5;103m█[48;5;110;38;5;110m██[48;5;103m▄[48;5;110;38;5;103m▄[48;5;67;38;5;110m▄[49;38;5;67m▄[39m [00m
|
||||||
|
[38;5;60m▄▄▄[48;5;60m█[48;5;67;38;5;67m█[38;5;60m▄[48;5;60;38;5;67m▄[48;5;67m█[38;5;60m▄▄▄[49m▀[39m [48;5;103;38;5;103m█[48;5;110;38;5;110m███[48;5;103;38;5;103m█[48;5;110;38;5;67m▄[48;5;67m█[49;39m [48;5;103;38;5;103m█[48;5;110;38;5;110m███[48;5;103;38;5;103m█[48;5;110;38;5;67m▄[48;5;67m█[49;39m [00m
|
||||||
|
[38;5;60m▀▀▀▀▀▀▀[39m [38;5;103m▀▀▀▀[39m [38;5;103m▀▀▀▀[39m [00m
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{#simplified version of full report#}
|
||||||
|
<b>{{ repository }} update</b>
|
||||||
|
{% for package in packages %}
|
||||||
|
<a href="{{ link_path }}/{{ package.filename }}">{{ package.name }}</a> {{ package.version }}
|
||||||
|
{% endfor %}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/js-md5@0.8.3/src/md5.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.33.0/tableExport.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/bootstrap-table.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/export/bootstrap-table-export.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/resizable/bootstrap-table-resizable.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/filter-control/bootstrap-table-filter-control.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/cookie/bootstrap-table-cookie.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function copyToClipboard(text, button) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
button.innerHTML = "<i class=\"bi bi-clipboard-check\"></i> copied";
|
||||||
|
setTimeout(_ => {
|
||||||
|
button.innerHTML = "<i class=\"bi bi-clipboard\"></i> copy";
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDataList(data, column) {
|
||||||
|
const elements = data.flatMap(row => row[column].split("<br>")).filter(v => v); // remove empty elements from array
|
||||||
|
return Array.from(new Set(elements)).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractListProperties(description, property) {
|
||||||
|
return Object.values(description.packages)
|
||||||
|
.map(pkg => pkg[property])
|
||||||
|
.reduce((left, right) => left.concat(right), []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterContains(text, value) {
|
||||||
|
return value.includes(text.toLowerCase().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterDateRange(text, value) {
|
||||||
|
const asOfStartOfDay = date => date.setUTCHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const [minDate, maxDate] = text.split(" - ");
|
||||||
|
const buildDate = asOfStartOfDay(new Date(value));
|
||||||
|
|
||||||
|
return (buildDate >= new Date(minDate)) && (buildDate <= new Date(maxDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterList(index, value, field, data) {
|
||||||
|
const dataList = extractDataList(data, field);
|
||||||
|
// the library removes all symbols from string, so it is just string
|
||||||
|
return value.includes(dataList[index].toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasActiveSelection() {
|
||||||
|
return !document.getSelection().isCollapsed; // not sure if it is a valid way, but I guess so
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasActiveDropdown() {
|
||||||
|
return Array.from(document.querySelectorAll(".dropdown-menu"))
|
||||||
|
.some(el => el.classList.contains("show"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasActiveModal() {
|
||||||
|
return Array.from(document.querySelectorAll(".modal"))
|
||||||
|
.some(el => el.classList.contains("show"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function headerClass(status) {
|
||||||
|
if (status === "pending") return ["bg-warning"];
|
||||||
|
if (status === "building") return ["bg-warning"];
|
||||||
|
if (status === "failed") return ["bg-danger", "text-white"];
|
||||||
|
if (status === "success") return ["bg-success", "text-white"];
|
||||||
|
return ["bg-secondary", "text-white"];
|
||||||
|
}
|
||||||
|
|
||||||
|
function listToTable(data) {
|
||||||
|
return Array.from(new Set(data))
|
||||||
|
.sort()
|
||||||
|
.map(entry => safe(entry))
|
||||||
|
.join("<br>");
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRequest(url, params, onSuccess, onFailure) {
|
||||||
|
const requestParams = {
|
||||||
|
method: params.method,
|
||||||
|
body: params.json ? JSON.stringify(params.json) : params.json,
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (params.query) {
|
||||||
|
const query = new URLSearchParams(params.query);
|
||||||
|
url += `?${query.toString()}`;
|
||||||
|
}
|
||||||
|
const convert = params.convert ?? (response => response.text());
|
||||||
|
|
||||||
|
return fetch(url, requestParams)
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
return convert(response);
|
||||||
|
} else {
|
||||||
|
const error = new Error("Network request error");
|
||||||
|
error.status = response.status;
|
||||||
|
error.statusText = response.statusText;
|
||||||
|
return response.text().then(text => {
|
||||||
|
error.text = text;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(data => onSuccess && onSuccess(data))
|
||||||
|
.catch(error => onFailure && onFailure(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readOptional(extractor, callback) {
|
||||||
|
for (let value = extractor(); !!value; value = null) {
|
||||||
|
callback(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ready(fn) {
|
||||||
|
if (document.readyState === "complete" || document.readyState === "interactive") {
|
||||||
|
setTimeout(fn, 1);
|
||||||
|
} else {
|
||||||
|
document.addEventListener("DOMContentLoaded", fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreAutoReloadSettings(toggle, intervalSelector) {
|
||||||
|
readOptional(() => localStorage.getItem(`ahriman-${toggle.id}-refresh-enabled`), value => toggle.checked = value === "true");
|
||||||
|
readOptional(() => localStorage.getItem(`ahriman-${toggle.id}-refresh-interval`), value => toggleActiveElement(intervalSelector, "interval", value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function safe(string) {
|
||||||
|
return String(string)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
// because I'm tired of safe element generation
|
||||||
|
function safeLink(url, text, title) {
|
||||||
|
const element = document.createElement("a");
|
||||||
|
element.href = url;
|
||||||
|
element.innerText = text;
|
||||||
|
if (title) {
|
||||||
|
element.title = title;
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleActiveElement(selector, dataType, value) {
|
||||||
|
const targetElement = selector.querySelector(`a[data-${dataType}="${value}"]`);
|
||||||
|
if (targetElement?.classList?.contains("active")) {
|
||||||
|
return; // element is already active, skip processing
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.from(selector.children).forEach(il => {
|
||||||
|
Array.from(il.children).forEach(el => el.classList.remove("active"));
|
||||||
|
});
|
||||||
|
targetElement?.classList?.add("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAutoReload(toggle, interval, intervalSelector, callback) {
|
||||||
|
if (interval) {
|
||||||
|
toggle.checked = true; // toggle reload
|
||||||
|
} else {
|
||||||
|
interval = intervalSelector.querySelector(".active")?.dataset?.interval; // find active element
|
||||||
|
}
|
||||||
|
|
||||||
|
let intervalId = null;
|
||||||
|
if (interval) {
|
||||||
|
if (toggle.checked) {
|
||||||
|
// refresh UI
|
||||||
|
toggleActiveElement(intervalSelector, "interval", interval);
|
||||||
|
// finally create timer task
|
||||||
|
intervalId = setInterval(callback, interval);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toggle.checked = false; // no active interval found, disable toggle
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(`ahriman-${toggle.id}-refresh-enabled`, toggle.checked);
|
||||||
|
localStorage.setItem(`ahriman-${toggle.id}-refresh-interval`, interval);
|
||||||
|
return intervalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTable(table, rows, rowChangedKey) {
|
||||||
|
// instead of using load method here, we just update rows manually to avoid table reinitialization
|
||||||
|
const currentData = table.bootstrapTable("getData").reduce((accumulator, row) => {
|
||||||
|
accumulator[row.id] = {state: row["0"], key: rowChangedKey(row)};
|
||||||
|
return accumulator;
|
||||||
|
}, {});
|
||||||
|
// insert or update rows, skipping ones whose status hasn't changed
|
||||||
|
rows.forEach(row => {
|
||||||
|
if (Object.hasOwn(currentData, row.id)) {
|
||||||
|
if (rowChangedKey(row) === currentData[row.id].key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
row["0"] = currentData[row.id].state; // copy checkbox state
|
||||||
|
table.bootstrapTable("updateByUniqueId", {
|
||||||
|
id: row.id,
|
||||||
|
row: row,
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
table.bootstrapTable("insertRow", {index: 0, row: row});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// remove old rows
|
||||||
|
const newData = rows.map(value => value.id);
|
||||||
|
Object.keys(currentData).forEach(id => {
|
||||||
|
if (!newData.includes(id)) {
|
||||||
|
table.bootstrapTable("removeByUniqueId", id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.prototype.equals = function (right, comparator) {
|
||||||
|
let index = this.length;
|
||||||
|
if (index !== right.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (index--) {
|
||||||
|
if (!comparator(this[index], right[index])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Date.prototype.toISOStringShort = function () {
|
||||||
|
const pad = number => String(number).padStart(2, "0");
|
||||||
|
return `${this.getFullYear()}-${pad(this.getMonth() + 1)}-${pad(this.getDate())} ${pad(this.getHours())}:${pad(this.getMinutes())}:${pad(this.getSeconds())}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" crossorigin="anonymous" type="text/css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/bootstrap-table.min.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/filter-control/bootstrap-table-filter-control.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.7/dist/cosmo/bootstrap.min.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/github.min.css" crossorigin="anonymous" type="text/css">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.pre-scrollable {
|
||||||
|
display: block;
|
||||||
|
max-height: 680px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[class*="language-"] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[class*="language-"] button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,715 @@
|
|||||||
|
# AUTOMATICALLY GENERATED by `shtab`
|
||||||
|
|
||||||
|
_shtab_ahriman_subparsers=('add' 'aur-search' 'check' 'clean' 'config' 'config-validate' 'copy' 'daemon' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'init' 'key-import' 'package-add' 'package-changes' 'package-changes-remove' 'package-copy' 'package-remove' 'package-status' 'package-status-remove' 'package-status-update' 'package-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'rebuild' 'remove' 'remove-unknown' 'repo-backup' 'repo-check' 'repo-clean' 'repo-config' 'repo-config-validate' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'repo-init' 'repo-rebuild' 'repo-remove-unknown' 'repo-report' 'repo-restore' 'repo-setup' 'repo-sign' 'repo-statistics' 'repo-status-update' 'repo-sync' 'repo-tree' 'repo-triggers' 'repo-update' 'report' 'run' 'search' 'service-clean' 'service-config' 'service-config-validate' 'service-key-import' 'service-repositories' 'service-run' 'service-setup' 'service-shell' 'service-tree-migrate' 'setup' 'shell' 'sign' 'status' 'status-update' 'sync' 'update' 'user-add' 'user-list' 'user-remove' 'version' 'web' 'web-reload')
|
||||||
|
|
||||||
|
_shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--configuration' '--force' '-l' '--lock' '--log-handler' '-q' '--quiet' '--report' '--no-report' '-r' '--repository' '--unsafe' '-V' '--version' '--wait-timeout')
|
||||||
|
_shtab_ahriman_add_option_strings=('-h' '--help' '--changes' '--no-changes' '--dependencies' '--no-dependencies' '-e' '--exit-code' '--increment' '--no-increment' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username' '-v' '--variable')
|
||||||
|
_shtab_ahriman_aur_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by')
|
||||||
|
_shtab_ahriman_check_option_strings=('-h' '--help' '--changes' '--no-changes' '--check-files' '--no-check-files' '-e' '--exit-code' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||||
|
_shtab_ahriman_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
|
||||||
|
_shtab_ahriman_config_option_strings=('-h' '--help' '--info' '--no-info' '--secure' '--no-secure')
|
||||||
|
_shtab_ahriman_config_validate_option_strings=('-h' '--help' '-e' '--exit-code')
|
||||||
|
_shtab_ahriman_copy_option_strings=('-h' '--help' '-e' '--exit-code' '--remove')
|
||||||
|
_shtab_ahriman_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--changes' '--no-changes' '--check-files' '--no-check-files' '--dependencies' '--no-dependencies' '--dry-run' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '--partitions' '--no-partitions' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||||
|
_shtab_ahriman_help_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_help_commands_unsafe_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_help_updates_option_strings=('-h' '--help' '-e' '--exit-code')
|
||||||
|
_shtab_ahriman_help_version_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_init_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
|
||||||
|
_shtab_ahriman_key_import_option_strings=('-h' '--help' '--key-server')
|
||||||
|
_shtab_ahriman_package_add_option_strings=('-h' '--help' '--changes' '--no-changes' '--dependencies' '--no-dependencies' '-e' '--exit-code' '--increment' '--no-increment' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username' '-v' '--variable')
|
||||||
|
_shtab_ahriman_package_changes_option_strings=('-h' '--help' '-e' '--exit-code')
|
||||||
|
_shtab_ahriman_package_changes_remove_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_package_copy_option_strings=('-h' '--help' '-e' '--exit-code' '--remove')
|
||||||
|
_shtab_ahriman_package_remove_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_package_status_option_strings=('-h' '--help' '--ahriman' '-e' '--exit-code' '--info' '--no-info' '-s' '--status')
|
||||||
|
_shtab_ahriman_package_status_remove_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_package_status_update_option_strings=('-h' '--help' '-s' '--status')
|
||||||
|
_shtab_ahriman_package_update_option_strings=('-h' '--help' '--changes' '--no-changes' '--dependencies' '--no-dependencies' '-e' '--exit-code' '--increment' '--no-increment' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username' '-v' '--variable')
|
||||||
|
_shtab_ahriman_patch_add_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_patch_list_option_strings=('-h' '--help' '-e' '--exit-code' '-v' '--variable')
|
||||||
|
_shtab_ahriman_patch_remove_option_strings=('-h' '--help' '-v' '--variable')
|
||||||
|
_shtab_ahriman_patch_set_add_option_strings=('-h' '--help' '-t' '--track')
|
||||||
|
_shtab_ahriman_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '--increment' '--no-increment' '-e' '--exit-code' '-s' '--status' '-u' '--username')
|
||||||
|
_shtab_ahriman_remove_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_remove_unknown_option_strings=('-h' '--help' '--dry-run')
|
||||||
|
_shtab_ahriman_repo_backup_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_repo_check_option_strings=('-h' '--help' '--changes' '--no-changes' '--check-files' '--no-check-files' '-e' '--exit-code' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||||
|
_shtab_ahriman_repo_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
|
||||||
|
_shtab_ahriman_repo_config_option_strings=('-h' '--help' '--info' '--no-info' '--secure' '--no-secure')
|
||||||
|
_shtab_ahriman_repo_config_validate_option_strings=('-h' '--help' '-e' '--exit-code')
|
||||||
|
_shtab_ahriman_repo_create_keyring_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_repo_create_mirrorlist_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_repo_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--changes' '--no-changes' '--check-files' '--no-check-files' '--dependencies' '--no-dependencies' '--dry-run' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '--partitions' '--no-partitions' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||||
|
_shtab_ahriman_repo_init_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
|
||||||
|
_shtab_ahriman_repo_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '--increment' '--no-increment' '-e' '--exit-code' '-s' '--status' '-u' '--username')
|
||||||
|
_shtab_ahriman_repo_remove_unknown_option_strings=('-h' '--help' '--dry-run')
|
||||||
|
_shtab_ahriman_repo_report_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_repo_restore_option_strings=('-h' '--help' '-o' '--output')
|
||||||
|
_shtab_ahriman_repo_setup_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
|
||||||
|
_shtab_ahriman_repo_sign_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_repo_statistics_option_strings=('-h' '--help' '--chart' '-e' '--event' '--from-date' '--limit' '--offset' '--to-date')
|
||||||
|
_shtab_ahriman_repo_status_update_option_strings=('-h' '--help' '-s' '--status')
|
||||||
|
_shtab_ahriman_repo_sync_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_repo_tree_option_strings=('-h' '--help' '-p' '--partitions')
|
||||||
|
_shtab_ahriman_repo_triggers_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_repo_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--changes' '--no-changes' '--check-files' '--no-check-files' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||||
|
_shtab_ahriman_report_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_run_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by')
|
||||||
|
_shtab_ahriman_service_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
|
||||||
|
_shtab_ahriman_service_config_option_strings=('-h' '--help' '--info' '--no-info' '--secure' '--no-secure')
|
||||||
|
_shtab_ahriman_service_config_validate_option_strings=('-h' '--help' '-e' '--exit-code')
|
||||||
|
_shtab_ahriman_service_key_import_option_strings=('-h' '--help' '--key-server')
|
||||||
|
_shtab_ahriman_service_repositories_option_strings=('-h' '--help' '--id-only' '--no-id-only')
|
||||||
|
_shtab_ahriman_service_run_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_service_setup_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
|
||||||
|
_shtab_ahriman_service_shell_option_strings=('-h' '--help' '-o' '--output')
|
||||||
|
_shtab_ahriman_service_tree_migrate_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_setup_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
|
||||||
|
_shtab_ahriman_shell_option_strings=('-h' '--help' '-o' '--output')
|
||||||
|
_shtab_ahriman_sign_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_status_option_strings=('-h' '--help' '--ahriman' '-e' '--exit-code' '--info' '--no-info' '-s' '--status')
|
||||||
|
_shtab_ahriman_status_update_option_strings=('-h' '--help' '-s' '--status')
|
||||||
|
_shtab_ahriman_sync_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--changes' '--no-changes' '--check-files' '--no-check-files' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||||
|
_shtab_ahriman_user_add_option_strings=('-h' '--help' '--key' '--packager' '-p' '--password' '-R' '--role')
|
||||||
|
_shtab_ahriman_user_list_option_strings=('-h' '--help' '-e' '--exit-code' '-R' '--role')
|
||||||
|
_shtab_ahriman_user_remove_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_version_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_web_option_strings=('-h' '--help')
|
||||||
|
_shtab_ahriman_web_reload_option_strings=('-h' '--help')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_shtab_ahriman_pos_0_choices=('add' 'aur-search' 'check' 'clean' 'config' 'config-validate' 'copy' 'daemon' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'init' 'key-import' 'package-add' 'package-changes' 'package-changes-remove' 'package-copy' 'package-remove' 'package-status' 'package-status-remove' 'package-status-update' 'package-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'rebuild' 'remove' 'remove-unknown' 'repo-backup' 'repo-check' 'repo-clean' 'repo-config' 'repo-config-validate' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'repo-init' 'repo-rebuild' 'repo-remove-unknown' 'repo-report' 'repo-restore' 'repo-setup' 'repo-sign' 'repo-statistics' 'repo-status-update' 'repo-sync' 'repo-tree' 'repo-triggers' 'repo-update' 'report' 'run' 'search' 'service-clean' 'service-config' 'service-config-validate' 'service-key-import' 'service-repositories' 'service-run' 'service-setup' 'service-shell' 'service-tree-migrate' 'setup' 'shell' 'sign' 'status' 'status-update' 'sync' 'update' 'user-add' 'user-list' 'user-remove' 'version' 'web' 'web-reload')
|
||||||
|
_shtab_ahriman___log_handler_choices=('console' 'syslog' 'journald')
|
||||||
|
_shtab_ahriman_add__s_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository')
|
||||||
|
_shtab_ahriman_add___source_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository')
|
||||||
|
_shtab_ahriman_aur_search___sort_by_choices=('description' 'first_submitted' 'id' 'last_modified' 'maintainer' 'name' 'num_votes' 'out_of_date' 'package_base' 'package_base_id' 'popularity' 'repository' 'submitter' 'url' 'url_path' 'version')
|
||||||
|
_shtab_ahriman_init___sign_target_choices=('disabled' 'packages' 'repository')
|
||||||
|
_shtab_ahriman_package_add__s_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository')
|
||||||
|
_shtab_ahriman_package_add___source_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository')
|
||||||
|
_shtab_ahriman_package_status__s_choices=('unknown' 'pending' 'building' 'failed' 'success')
|
||||||
|
_shtab_ahriman_package_status___status_choices=('unknown' 'pending' 'building' 'failed' 'success')
|
||||||
|
_shtab_ahriman_package_status_update__s_choices=('unknown' 'pending' 'building' 'failed' 'success')
|
||||||
|
_shtab_ahriman_package_status_update___status_choices=('unknown' 'pending' 'building' 'failed' 'success')
|
||||||
|
_shtab_ahriman_package_update__s_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository')
|
||||||
|
_shtab_ahriman_package_update___source_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository')
|
||||||
|
_shtab_ahriman_rebuild__s_choices=('unknown' 'pending' 'building' 'failed' 'success')
|
||||||
|
_shtab_ahriman_rebuild___status_choices=('unknown' 'pending' 'building' 'failed' 'success')
|
||||||
|
_shtab_ahriman_repo_init___sign_target_choices=('disabled' 'packages' 'repository')
|
||||||
|
_shtab_ahriman_repo_rebuild__s_choices=('unknown' 'pending' 'building' 'failed' 'success')
|
||||||
|
_shtab_ahriman_repo_rebuild___status_choices=('unknown' 'pending' 'building' 'failed' 'success')
|
||||||
|
_shtab_ahriman_repo_setup___sign_target_choices=('disabled' 'packages' 'repository')
|
||||||
|
_shtab_ahriman_repo_statistics__e_choices=('package-outdated' 'package-removed' 'package-update-failed' 'package-updated')
|
||||||
|
_shtab_ahriman_repo_statistics___event_choices=('package-outdated' 'package-removed' 'package-update-failed' 'package-updated')
|
||||||
|
_shtab_ahriman_repo_status_update__s_choices=('unknown' 'pending' 'building' 'failed' 'success')
|
||||||
|
_shtab_ahriman_repo_status_update___status_choices=('unknown' 'pending' 'building' 'failed' 'success')
|
||||||
|
_shtab_ahriman_search___sort_by_choices=('description' 'first_submitted' 'id' 'last_modified' 'maintainer' 'name' 'num_votes' 'out_of_date' 'package_base' 'package_base_id' 'popularity' 'repository' 'submitter' 'url' 'url_path' 'version')
|
||||||
|
_shtab_ahriman_service_setup___sign_target_choices=('disabled' 'packages' 'repository')
|
||||||
|
_shtab_ahriman_setup___sign_target_choices=('disabled' 'packages' 'repository')
|
||||||
|
_shtab_ahriman_status__s_choices=('unknown' 'pending' 'building' 'failed' 'success')
|
||||||
|
_shtab_ahriman_status___status_choices=('unknown' 'pending' 'building' 'failed' 'success')
|
||||||
|
_shtab_ahriman_status_update__s_choices=('unknown' 'pending' 'building' 'failed' 'success')
|
||||||
|
_shtab_ahriman_status_update___status_choices=('unknown' 'pending' 'building' 'failed' 'success')
|
||||||
|
_shtab_ahriman_user_add__R_choices=('unauthorized' 'read' 'reporter' 'full')
|
||||||
|
_shtab_ahriman_user_add___role_choices=('unauthorized' 'read' 'reporter' 'full')
|
||||||
|
_shtab_ahriman_user_list__R_choices=('unauthorized' 'read' 'reporter' 'full')
|
||||||
|
_shtab_ahriman_user_list___role_choices=('unauthorized' 'read' 'reporter' 'full')
|
||||||
|
|
||||||
|
_shtab_ahriman_pos_0_nargs=A...
|
||||||
|
_shtab_ahriman__h_nargs=0
|
||||||
|
_shtab_ahriman___help_nargs=0
|
||||||
|
_shtab_ahriman___force_nargs=0
|
||||||
|
_shtab_ahriman__q_nargs=0
|
||||||
|
_shtab_ahriman___quiet_nargs=0
|
||||||
|
_shtab_ahriman___report_nargs=0
|
||||||
|
_shtab_ahriman___no_report_nargs=0
|
||||||
|
_shtab_ahriman___unsafe_nargs=0
|
||||||
|
_shtab_ahriman__V_nargs=0
|
||||||
|
_shtab_ahriman___version_nargs=0
|
||||||
|
_shtab_ahriman_add_pos_0_nargs=+
|
||||||
|
_shtab_ahriman_add__h_nargs=0
|
||||||
|
_shtab_ahriman_add___help_nargs=0
|
||||||
|
_shtab_ahriman_add___changes_nargs=0
|
||||||
|
_shtab_ahriman_add___no_changes_nargs=0
|
||||||
|
_shtab_ahriman_add___dependencies_nargs=0
|
||||||
|
_shtab_ahriman_add___no_dependencies_nargs=0
|
||||||
|
_shtab_ahriman_add__e_nargs=0
|
||||||
|
_shtab_ahriman_add___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_add___increment_nargs=0
|
||||||
|
_shtab_ahriman_add___no_increment_nargs=0
|
||||||
|
_shtab_ahriman_add__n_nargs=0
|
||||||
|
_shtab_ahriman_add___now_nargs=0
|
||||||
|
_shtab_ahriman_add__y_nargs=0
|
||||||
|
_shtab_ahriman_add___refresh_nargs=0
|
||||||
|
_shtab_ahriman_aur_search_pos_0_nargs=+
|
||||||
|
_shtab_ahriman_aur_search__h_nargs=0
|
||||||
|
_shtab_ahriman_aur_search___help_nargs=0
|
||||||
|
_shtab_ahriman_aur_search__e_nargs=0
|
||||||
|
_shtab_ahriman_aur_search___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_aur_search___info_nargs=0
|
||||||
|
_shtab_ahriman_aur_search___no_info_nargs=0
|
||||||
|
_shtab_ahriman_check_pos_0_nargs=*
|
||||||
|
_shtab_ahriman_check__h_nargs=0
|
||||||
|
_shtab_ahriman_check___help_nargs=0
|
||||||
|
_shtab_ahriman_check___changes_nargs=0
|
||||||
|
_shtab_ahriman_check___no_changes_nargs=0
|
||||||
|
_shtab_ahriman_check___check_files_nargs=0
|
||||||
|
_shtab_ahriman_check___no_check_files_nargs=0
|
||||||
|
_shtab_ahriman_check__e_nargs=0
|
||||||
|
_shtab_ahriman_check___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_check___vcs_nargs=0
|
||||||
|
_shtab_ahriman_check___no_vcs_nargs=0
|
||||||
|
_shtab_ahriman_check__y_nargs=0
|
||||||
|
_shtab_ahriman_check___refresh_nargs=0
|
||||||
|
_shtab_ahriman_clean__h_nargs=0
|
||||||
|
_shtab_ahriman_clean___help_nargs=0
|
||||||
|
_shtab_ahriman_clean___cache_nargs=0
|
||||||
|
_shtab_ahriman_clean___no_cache_nargs=0
|
||||||
|
_shtab_ahriman_clean___chroot_nargs=0
|
||||||
|
_shtab_ahriman_clean___no_chroot_nargs=0
|
||||||
|
_shtab_ahriman_clean___manual_nargs=0
|
||||||
|
_shtab_ahriman_clean___no_manual_nargs=0
|
||||||
|
_shtab_ahriman_clean___packages_nargs=0
|
||||||
|
_shtab_ahriman_clean___no_packages_nargs=0
|
||||||
|
_shtab_ahriman_clean___pacman_nargs=0
|
||||||
|
_shtab_ahriman_clean___no_pacman_nargs=0
|
||||||
|
_shtab_ahriman_config__h_nargs=0
|
||||||
|
_shtab_ahriman_config___help_nargs=0
|
||||||
|
_shtab_ahriman_config___info_nargs=0
|
||||||
|
_shtab_ahriman_config___no_info_nargs=0
|
||||||
|
_shtab_ahriman_config___secure_nargs=0
|
||||||
|
_shtab_ahriman_config___no_secure_nargs=0
|
||||||
|
_shtab_ahriman_config_validate__h_nargs=0
|
||||||
|
_shtab_ahriman_config_validate___help_nargs=0
|
||||||
|
_shtab_ahriman_config_validate__e_nargs=0
|
||||||
|
_shtab_ahriman_config_validate___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_copy_pos_1_nargs=+
|
||||||
|
_shtab_ahriman_copy__h_nargs=0
|
||||||
|
_shtab_ahriman_copy___help_nargs=0
|
||||||
|
_shtab_ahriman_copy__e_nargs=0
|
||||||
|
_shtab_ahriman_copy___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_copy___remove_nargs=0
|
||||||
|
_shtab_ahriman_daemon__h_nargs=0
|
||||||
|
_shtab_ahriman_daemon___help_nargs=0
|
||||||
|
_shtab_ahriman_daemon___aur_nargs=0
|
||||||
|
_shtab_ahriman_daemon___no_aur_nargs=0
|
||||||
|
_shtab_ahriman_daemon___changes_nargs=0
|
||||||
|
_shtab_ahriman_daemon___no_changes_nargs=0
|
||||||
|
_shtab_ahriman_daemon___check_files_nargs=0
|
||||||
|
_shtab_ahriman_daemon___no_check_files_nargs=0
|
||||||
|
_shtab_ahriman_daemon___dependencies_nargs=0
|
||||||
|
_shtab_ahriman_daemon___no_dependencies_nargs=0
|
||||||
|
_shtab_ahriman_daemon___dry_run_nargs=0
|
||||||
|
_shtab_ahriman_daemon___increment_nargs=0
|
||||||
|
_shtab_ahriman_daemon___no_increment_nargs=0
|
||||||
|
_shtab_ahriman_daemon___local_nargs=0
|
||||||
|
_shtab_ahriman_daemon___no_local_nargs=0
|
||||||
|
_shtab_ahriman_daemon___manual_nargs=0
|
||||||
|
_shtab_ahriman_daemon___no_manual_nargs=0
|
||||||
|
_shtab_ahriman_daemon___partitions_nargs=0
|
||||||
|
_shtab_ahriman_daemon___no_partitions_nargs=0
|
||||||
|
_shtab_ahriman_daemon___vcs_nargs=0
|
||||||
|
_shtab_ahriman_daemon___no_vcs_nargs=0
|
||||||
|
_shtab_ahriman_daemon__y_nargs=0
|
||||||
|
_shtab_ahriman_daemon___refresh_nargs=0
|
||||||
|
_shtab_ahriman_help__h_nargs=0
|
||||||
|
_shtab_ahriman_help___help_nargs=0
|
||||||
|
_shtab_ahriman_help_commands_unsafe_pos_0_nargs=*
|
||||||
|
_shtab_ahriman_help_commands_unsafe__h_nargs=0
|
||||||
|
_shtab_ahriman_help_commands_unsafe___help_nargs=0
|
||||||
|
_shtab_ahriman_help_updates__h_nargs=0
|
||||||
|
_shtab_ahriman_help_updates___help_nargs=0
|
||||||
|
_shtab_ahriman_help_updates__e_nargs=0
|
||||||
|
_shtab_ahriman_help_updates___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_help_version__h_nargs=0
|
||||||
|
_shtab_ahriman_help_version___help_nargs=0
|
||||||
|
_shtab_ahriman_init__h_nargs=0
|
||||||
|
_shtab_ahriman_init___help_nargs=0
|
||||||
|
_shtab_ahriman_init___generate_salt_nargs=0
|
||||||
|
_shtab_ahriman_init___no_generate_salt_nargs=0
|
||||||
|
_shtab_ahriman_init___makeflags_jobs_nargs=0
|
||||||
|
_shtab_ahriman_init___no_makeflags_jobs_nargs=0
|
||||||
|
_shtab_ahriman_init___multilib_nargs=0
|
||||||
|
_shtab_ahriman_init___no_multilib_nargs=0
|
||||||
|
_shtab_ahriman_key_import__h_nargs=0
|
||||||
|
_shtab_ahriman_key_import___help_nargs=0
|
||||||
|
_shtab_ahriman_package_add_pos_0_nargs=+
|
||||||
|
_shtab_ahriman_package_add__h_nargs=0
|
||||||
|
_shtab_ahriman_package_add___help_nargs=0
|
||||||
|
_shtab_ahriman_package_add___changes_nargs=0
|
||||||
|
_shtab_ahriman_package_add___no_changes_nargs=0
|
||||||
|
_shtab_ahriman_package_add___dependencies_nargs=0
|
||||||
|
_shtab_ahriman_package_add___no_dependencies_nargs=0
|
||||||
|
_shtab_ahriman_package_add__e_nargs=0
|
||||||
|
_shtab_ahriman_package_add___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_package_add___increment_nargs=0
|
||||||
|
_shtab_ahriman_package_add___no_increment_nargs=0
|
||||||
|
_shtab_ahriman_package_add__n_nargs=0
|
||||||
|
_shtab_ahriman_package_add___now_nargs=0
|
||||||
|
_shtab_ahriman_package_add__y_nargs=0
|
||||||
|
_shtab_ahriman_package_add___refresh_nargs=0
|
||||||
|
_shtab_ahriman_package_changes__h_nargs=0
|
||||||
|
_shtab_ahriman_package_changes___help_nargs=0
|
||||||
|
_shtab_ahriman_package_changes__e_nargs=0
|
||||||
|
_shtab_ahriman_package_changes___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_package_changes_remove__h_nargs=0
|
||||||
|
_shtab_ahriman_package_changes_remove___help_nargs=0
|
||||||
|
_shtab_ahriman_package_copy_pos_1_nargs=+
|
||||||
|
_shtab_ahriman_package_copy__h_nargs=0
|
||||||
|
_shtab_ahriman_package_copy___help_nargs=0
|
||||||
|
_shtab_ahriman_package_copy__e_nargs=0
|
||||||
|
_shtab_ahriman_package_copy___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_package_copy___remove_nargs=0
|
||||||
|
_shtab_ahriman_package_remove_pos_0_nargs=+
|
||||||
|
_shtab_ahriman_package_remove__h_nargs=0
|
||||||
|
_shtab_ahriman_package_remove___help_nargs=0
|
||||||
|
_shtab_ahriman_package_status_pos_0_nargs=*
|
||||||
|
_shtab_ahriman_package_status__h_nargs=0
|
||||||
|
_shtab_ahriman_package_status___help_nargs=0
|
||||||
|
_shtab_ahriman_package_status___ahriman_nargs=0
|
||||||
|
_shtab_ahriman_package_status__e_nargs=0
|
||||||
|
_shtab_ahriman_package_status___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_package_status___info_nargs=0
|
||||||
|
_shtab_ahriman_package_status___no_info_nargs=0
|
||||||
|
_shtab_ahriman_package_status_remove_pos_0_nargs=+
|
||||||
|
_shtab_ahriman_package_status_remove__h_nargs=0
|
||||||
|
_shtab_ahriman_package_status_remove___help_nargs=0
|
||||||
|
_shtab_ahriman_package_status_update_pos_0_nargs=*
|
||||||
|
_shtab_ahriman_package_status_update__h_nargs=0
|
||||||
|
_shtab_ahriman_package_status_update___help_nargs=0
|
||||||
|
_shtab_ahriman_package_update_pos_0_nargs=+
|
||||||
|
_shtab_ahriman_package_update__h_nargs=0
|
||||||
|
_shtab_ahriman_package_update___help_nargs=0
|
||||||
|
_shtab_ahriman_package_update___changes_nargs=0
|
||||||
|
_shtab_ahriman_package_update___no_changes_nargs=0
|
||||||
|
_shtab_ahriman_package_update___dependencies_nargs=0
|
||||||
|
_shtab_ahriman_package_update___no_dependencies_nargs=0
|
||||||
|
_shtab_ahriman_package_update__e_nargs=0
|
||||||
|
_shtab_ahriman_package_update___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_package_update___increment_nargs=0
|
||||||
|
_shtab_ahriman_package_update___no_increment_nargs=0
|
||||||
|
_shtab_ahriman_package_update__n_nargs=0
|
||||||
|
_shtab_ahriman_package_update___now_nargs=0
|
||||||
|
_shtab_ahriman_package_update__y_nargs=0
|
||||||
|
_shtab_ahriman_package_update___refresh_nargs=0
|
||||||
|
_shtab_ahriman_patch_add__h_nargs=0
|
||||||
|
_shtab_ahriman_patch_add___help_nargs=0
|
||||||
|
_shtab_ahriman_patch_list__h_nargs=0
|
||||||
|
_shtab_ahriman_patch_list___help_nargs=0
|
||||||
|
_shtab_ahriman_patch_list__e_nargs=0
|
||||||
|
_shtab_ahriman_patch_list___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_patch_remove__h_nargs=0
|
||||||
|
_shtab_ahriman_patch_remove___help_nargs=0
|
||||||
|
_shtab_ahriman_patch_set_add__h_nargs=0
|
||||||
|
_shtab_ahriman_patch_set_add___help_nargs=0
|
||||||
|
_shtab_ahriman_rebuild__h_nargs=0
|
||||||
|
_shtab_ahriman_rebuild___help_nargs=0
|
||||||
|
_shtab_ahriman_rebuild___dry_run_nargs=0
|
||||||
|
_shtab_ahriman_rebuild___from_database_nargs=0
|
||||||
|
_shtab_ahriman_rebuild___increment_nargs=0
|
||||||
|
_shtab_ahriman_rebuild___no_increment_nargs=0
|
||||||
|
_shtab_ahriman_rebuild__e_nargs=0
|
||||||
|
_shtab_ahriman_rebuild___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_remove_pos_0_nargs=+
|
||||||
|
_shtab_ahriman_remove__h_nargs=0
|
||||||
|
_shtab_ahriman_remove___help_nargs=0
|
||||||
|
_shtab_ahriman_remove_unknown__h_nargs=0
|
||||||
|
_shtab_ahriman_remove_unknown___help_nargs=0
|
||||||
|
_shtab_ahriman_remove_unknown___dry_run_nargs=0
|
||||||
|
_shtab_ahriman_repo_backup__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_backup___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_check_pos_0_nargs=*
|
||||||
|
_shtab_ahriman_repo_check__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_check___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_check___changes_nargs=0
|
||||||
|
_shtab_ahriman_repo_check___no_changes_nargs=0
|
||||||
|
_shtab_ahriman_repo_check___check_files_nargs=0
|
||||||
|
_shtab_ahriman_repo_check___no_check_files_nargs=0
|
||||||
|
_shtab_ahriman_repo_check__e_nargs=0
|
||||||
|
_shtab_ahriman_repo_check___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_repo_check___vcs_nargs=0
|
||||||
|
_shtab_ahriman_repo_check___no_vcs_nargs=0
|
||||||
|
_shtab_ahriman_repo_check__y_nargs=0
|
||||||
|
_shtab_ahriman_repo_check___refresh_nargs=0
|
||||||
|
_shtab_ahriman_repo_clean__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_clean___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_clean___cache_nargs=0
|
||||||
|
_shtab_ahriman_repo_clean___no_cache_nargs=0
|
||||||
|
_shtab_ahriman_repo_clean___chroot_nargs=0
|
||||||
|
_shtab_ahriman_repo_clean___no_chroot_nargs=0
|
||||||
|
_shtab_ahriman_repo_clean___manual_nargs=0
|
||||||
|
_shtab_ahriman_repo_clean___no_manual_nargs=0
|
||||||
|
_shtab_ahriman_repo_clean___packages_nargs=0
|
||||||
|
_shtab_ahriman_repo_clean___no_packages_nargs=0
|
||||||
|
_shtab_ahriman_repo_clean___pacman_nargs=0
|
||||||
|
_shtab_ahriman_repo_clean___no_pacman_nargs=0
|
||||||
|
_shtab_ahriman_repo_config__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_config___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_config___info_nargs=0
|
||||||
|
_shtab_ahriman_repo_config___no_info_nargs=0
|
||||||
|
_shtab_ahriman_repo_config___secure_nargs=0
|
||||||
|
_shtab_ahriman_repo_config___no_secure_nargs=0
|
||||||
|
_shtab_ahriman_repo_config_validate__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_config_validate___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_config_validate__e_nargs=0
|
||||||
|
_shtab_ahriman_repo_config_validate___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_repo_create_keyring__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_create_keyring___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_create_mirrorlist__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_create_mirrorlist___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___aur_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___no_aur_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___changes_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___no_changes_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___check_files_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___no_check_files_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___dependencies_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___no_dependencies_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___dry_run_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___increment_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___no_increment_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___local_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___no_local_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___manual_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___no_manual_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___partitions_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___no_partitions_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___vcs_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___no_vcs_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon__y_nargs=0
|
||||||
|
_shtab_ahriman_repo_daemon___refresh_nargs=0
|
||||||
|
_shtab_ahriman_repo_init__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_init___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_init___generate_salt_nargs=0
|
||||||
|
_shtab_ahriman_repo_init___no_generate_salt_nargs=0
|
||||||
|
_shtab_ahriman_repo_init___makeflags_jobs_nargs=0
|
||||||
|
_shtab_ahriman_repo_init___no_makeflags_jobs_nargs=0
|
||||||
|
_shtab_ahriman_repo_init___multilib_nargs=0
|
||||||
|
_shtab_ahriman_repo_init___no_multilib_nargs=0
|
||||||
|
_shtab_ahriman_repo_rebuild__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_rebuild___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_rebuild___dry_run_nargs=0
|
||||||
|
_shtab_ahriman_repo_rebuild___from_database_nargs=0
|
||||||
|
_shtab_ahriman_repo_rebuild___increment_nargs=0
|
||||||
|
_shtab_ahriman_repo_rebuild___no_increment_nargs=0
|
||||||
|
_shtab_ahriman_repo_rebuild__e_nargs=0
|
||||||
|
_shtab_ahriman_repo_rebuild___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_repo_remove_unknown__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_remove_unknown___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_remove_unknown___dry_run_nargs=0
|
||||||
|
_shtab_ahriman_repo_report__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_report___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_restore__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_restore___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_setup__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_setup___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_setup___generate_salt_nargs=0
|
||||||
|
_shtab_ahriman_repo_setup___no_generate_salt_nargs=0
|
||||||
|
_shtab_ahriman_repo_setup___makeflags_jobs_nargs=0
|
||||||
|
_shtab_ahriman_repo_setup___no_makeflags_jobs_nargs=0
|
||||||
|
_shtab_ahriman_repo_setup___multilib_nargs=0
|
||||||
|
_shtab_ahriman_repo_setup___no_multilib_nargs=0
|
||||||
|
_shtab_ahriman_repo_sign_pos_0_nargs=*
|
||||||
|
_shtab_ahriman_repo_sign__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_sign___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_statistics__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_statistics___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_status_update__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_status_update___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_sync__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_sync___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_tree__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_tree___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_triggers_pos_0_nargs=*
|
||||||
|
_shtab_ahriman_repo_triggers__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_triggers___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_update_pos_0_nargs=*
|
||||||
|
_shtab_ahriman_repo_update__h_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___help_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___aur_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___no_aur_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___changes_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___no_changes_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___check_files_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___no_check_files_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___dependencies_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___no_dependencies_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___dry_run_nargs=0
|
||||||
|
_shtab_ahriman_repo_update__e_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___increment_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___no_increment_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___local_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___no_local_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___manual_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___no_manual_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___vcs_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___no_vcs_nargs=0
|
||||||
|
_shtab_ahriman_repo_update__y_nargs=0
|
||||||
|
_shtab_ahriman_repo_update___refresh_nargs=0
|
||||||
|
_shtab_ahriman_report__h_nargs=0
|
||||||
|
_shtab_ahriman_report___help_nargs=0
|
||||||
|
_shtab_ahriman_run_pos_0_nargs=+
|
||||||
|
_shtab_ahriman_run__h_nargs=0
|
||||||
|
_shtab_ahriman_run___help_nargs=0
|
||||||
|
_shtab_ahriman_search_pos_0_nargs=+
|
||||||
|
_shtab_ahriman_search__h_nargs=0
|
||||||
|
_shtab_ahriman_search___help_nargs=0
|
||||||
|
_shtab_ahriman_search__e_nargs=0
|
||||||
|
_shtab_ahriman_search___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_search___info_nargs=0
|
||||||
|
_shtab_ahriman_search___no_info_nargs=0
|
||||||
|
_shtab_ahriman_service_clean__h_nargs=0
|
||||||
|
_shtab_ahriman_service_clean___help_nargs=0
|
||||||
|
_shtab_ahriman_service_clean___cache_nargs=0
|
||||||
|
_shtab_ahriman_service_clean___no_cache_nargs=0
|
||||||
|
_shtab_ahriman_service_clean___chroot_nargs=0
|
||||||
|
_shtab_ahriman_service_clean___no_chroot_nargs=0
|
||||||
|
_shtab_ahriman_service_clean___manual_nargs=0
|
||||||
|
_shtab_ahriman_service_clean___no_manual_nargs=0
|
||||||
|
_shtab_ahriman_service_clean___packages_nargs=0
|
||||||
|
_shtab_ahriman_service_clean___no_packages_nargs=0
|
||||||
|
_shtab_ahriman_service_clean___pacman_nargs=0
|
||||||
|
_shtab_ahriman_service_clean___no_pacman_nargs=0
|
||||||
|
_shtab_ahriman_service_config__h_nargs=0
|
||||||
|
_shtab_ahriman_service_config___help_nargs=0
|
||||||
|
_shtab_ahriman_service_config___info_nargs=0
|
||||||
|
_shtab_ahriman_service_config___no_info_nargs=0
|
||||||
|
_shtab_ahriman_service_config___secure_nargs=0
|
||||||
|
_shtab_ahriman_service_config___no_secure_nargs=0
|
||||||
|
_shtab_ahriman_service_config_validate__h_nargs=0
|
||||||
|
_shtab_ahriman_service_config_validate___help_nargs=0
|
||||||
|
_shtab_ahriman_service_config_validate__e_nargs=0
|
||||||
|
_shtab_ahriman_service_config_validate___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_service_key_import__h_nargs=0
|
||||||
|
_shtab_ahriman_service_key_import___help_nargs=0
|
||||||
|
_shtab_ahriman_service_repositories__h_nargs=0
|
||||||
|
_shtab_ahriman_service_repositories___help_nargs=0
|
||||||
|
_shtab_ahriman_service_repositories___id_only_nargs=0
|
||||||
|
_shtab_ahriman_service_repositories___no_id_only_nargs=0
|
||||||
|
_shtab_ahriman_service_run_pos_0_nargs=+
|
||||||
|
_shtab_ahriman_service_run__h_nargs=0
|
||||||
|
_shtab_ahriman_service_run___help_nargs=0
|
||||||
|
_shtab_ahriman_service_setup__h_nargs=0
|
||||||
|
_shtab_ahriman_service_setup___help_nargs=0
|
||||||
|
_shtab_ahriman_service_setup___generate_salt_nargs=0
|
||||||
|
_shtab_ahriman_service_setup___no_generate_salt_nargs=0
|
||||||
|
_shtab_ahriman_service_setup___makeflags_jobs_nargs=0
|
||||||
|
_shtab_ahriman_service_setup___no_makeflags_jobs_nargs=0
|
||||||
|
_shtab_ahriman_service_setup___multilib_nargs=0
|
||||||
|
_shtab_ahriman_service_setup___no_multilib_nargs=0
|
||||||
|
_shtab_ahriman_service_shell__h_nargs=0
|
||||||
|
_shtab_ahriman_service_shell___help_nargs=0
|
||||||
|
_shtab_ahriman_service_shell__v_nargs=0
|
||||||
|
_shtab_ahriman_service_shell___verbose_nargs=0
|
||||||
|
_shtab_ahriman_service_tree_migrate__h_nargs=0
|
||||||
|
_shtab_ahriman_service_tree_migrate___help_nargs=0
|
||||||
|
_shtab_ahriman_setup__h_nargs=0
|
||||||
|
_shtab_ahriman_setup___help_nargs=0
|
||||||
|
_shtab_ahriman_setup___generate_salt_nargs=0
|
||||||
|
_shtab_ahriman_setup___no_generate_salt_nargs=0
|
||||||
|
_shtab_ahriman_setup___makeflags_jobs_nargs=0
|
||||||
|
_shtab_ahriman_setup___no_makeflags_jobs_nargs=0
|
||||||
|
_shtab_ahriman_setup___multilib_nargs=0
|
||||||
|
_shtab_ahriman_setup___no_multilib_nargs=0
|
||||||
|
_shtab_ahriman_shell__h_nargs=0
|
||||||
|
_shtab_ahriman_shell___help_nargs=0
|
||||||
|
_shtab_ahriman_shell__v_nargs=0
|
||||||
|
_shtab_ahriman_shell___verbose_nargs=0
|
||||||
|
_shtab_ahriman_sign_pos_0_nargs=*
|
||||||
|
_shtab_ahriman_sign__h_nargs=0
|
||||||
|
_shtab_ahriman_sign___help_nargs=0
|
||||||
|
_shtab_ahriman_status_pos_0_nargs=*
|
||||||
|
_shtab_ahriman_status__h_nargs=0
|
||||||
|
_shtab_ahriman_status___help_nargs=0
|
||||||
|
_shtab_ahriman_status___ahriman_nargs=0
|
||||||
|
_shtab_ahriman_status__e_nargs=0
|
||||||
|
_shtab_ahriman_status___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_status___info_nargs=0
|
||||||
|
_shtab_ahriman_status___no_info_nargs=0
|
||||||
|
_shtab_ahriman_status_update_pos_0_nargs=*
|
||||||
|
_shtab_ahriman_status_update__h_nargs=0
|
||||||
|
_shtab_ahriman_status_update___help_nargs=0
|
||||||
|
_shtab_ahriman_sync__h_nargs=0
|
||||||
|
_shtab_ahriman_sync___help_nargs=0
|
||||||
|
_shtab_ahriman_update_pos_0_nargs=*
|
||||||
|
_shtab_ahriman_update__h_nargs=0
|
||||||
|
_shtab_ahriman_update___help_nargs=0
|
||||||
|
_shtab_ahriman_update___aur_nargs=0
|
||||||
|
_shtab_ahriman_update___no_aur_nargs=0
|
||||||
|
_shtab_ahriman_update___changes_nargs=0
|
||||||
|
_shtab_ahriman_update___no_changes_nargs=0
|
||||||
|
_shtab_ahriman_update___check_files_nargs=0
|
||||||
|
_shtab_ahriman_update___no_check_files_nargs=0
|
||||||
|
_shtab_ahriman_update___dependencies_nargs=0
|
||||||
|
_shtab_ahriman_update___no_dependencies_nargs=0
|
||||||
|
_shtab_ahriman_update___dry_run_nargs=0
|
||||||
|
_shtab_ahriman_update__e_nargs=0
|
||||||
|
_shtab_ahriman_update___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_update___increment_nargs=0
|
||||||
|
_shtab_ahriman_update___no_increment_nargs=0
|
||||||
|
_shtab_ahriman_update___local_nargs=0
|
||||||
|
_shtab_ahriman_update___no_local_nargs=0
|
||||||
|
_shtab_ahriman_update___manual_nargs=0
|
||||||
|
_shtab_ahriman_update___no_manual_nargs=0
|
||||||
|
_shtab_ahriman_update___vcs_nargs=0
|
||||||
|
_shtab_ahriman_update___no_vcs_nargs=0
|
||||||
|
_shtab_ahriman_update__y_nargs=0
|
||||||
|
_shtab_ahriman_update___refresh_nargs=0
|
||||||
|
_shtab_ahriman_user_add__h_nargs=0
|
||||||
|
_shtab_ahriman_user_add___help_nargs=0
|
||||||
|
_shtab_ahriman_user_list__h_nargs=0
|
||||||
|
_shtab_ahriman_user_list___help_nargs=0
|
||||||
|
_shtab_ahriman_user_list__e_nargs=0
|
||||||
|
_shtab_ahriman_user_list___exit_code_nargs=0
|
||||||
|
_shtab_ahriman_user_remove__h_nargs=0
|
||||||
|
_shtab_ahriman_user_remove___help_nargs=0
|
||||||
|
_shtab_ahriman_version__h_nargs=0
|
||||||
|
_shtab_ahriman_version___help_nargs=0
|
||||||
|
_shtab_ahriman_web__h_nargs=0
|
||||||
|
_shtab_ahriman_web___help_nargs=0
|
||||||
|
_shtab_ahriman_web_reload__h_nargs=0
|
||||||
|
_shtab_ahriman_web_reload___help_nargs=0
|
||||||
|
|
||||||
|
|
||||||
|
# $1=COMP_WORDS[1]
|
||||||
|
_shtab_compgen_files() {
|
||||||
|
compgen -f -- $1 # files
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1=COMP_WORDS[1]
|
||||||
|
_shtab_compgen_dirs() {
|
||||||
|
compgen -d -- $1 # recurse into subdirs
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1=COMP_WORDS[1]
|
||||||
|
_shtab_replace_nonword() {
|
||||||
|
echo "${1//[^[:word:]]/_}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# set default values (called for the initial parser & any subparsers)
|
||||||
|
_set_parser_defaults() {
|
||||||
|
local subparsers_var="${prefix}_subparsers[@]"
|
||||||
|
sub_parsers=${!subparsers_var-}
|
||||||
|
|
||||||
|
local current_option_strings_var="${prefix}_option_strings[@]"
|
||||||
|
current_option_strings=${!current_option_strings_var}
|
||||||
|
|
||||||
|
completed_positional_actions=0
|
||||||
|
|
||||||
|
_set_new_action "pos_${completed_positional_actions}" true
|
||||||
|
}
|
||||||
|
|
||||||
|
# $1=action identifier
|
||||||
|
# $2=positional action (bool)
|
||||||
|
# set all identifiers for an action's parameters
|
||||||
|
_set_new_action() {
|
||||||
|
current_action="${prefix}_$(_shtab_replace_nonword $1)"
|
||||||
|
|
||||||
|
local current_action_compgen_var=${current_action}_COMPGEN
|
||||||
|
current_action_compgen="${!current_action_compgen_var-}"
|
||||||
|
|
||||||
|
local current_action_choices_var="${current_action}_choices[@]"
|
||||||
|
current_action_choices="${!current_action_choices_var-}"
|
||||||
|
|
||||||
|
local current_action_nargs_var="${current_action}_nargs"
|
||||||
|
if [ -n "${!current_action_nargs_var-}" ]; then
|
||||||
|
current_action_nargs="${!current_action_nargs_var}"
|
||||||
|
else
|
||||||
|
current_action_nargs=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
current_action_args_start_index=$(( $word_index + 1 - $pos_only ))
|
||||||
|
|
||||||
|
current_action_is_positional=$2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Notes:
|
||||||
|
# `COMPREPLY`: what will be rendered after completion is triggered
|
||||||
|
# `completing_word`: currently typed word to generate completions for
|
||||||
|
# `${!var}`: evaluates the content of `var` and expand its content as a variable
|
||||||
|
# hello="world"
|
||||||
|
# x="hello"
|
||||||
|
# ${!x} -> ${hello} -> "world"
|
||||||
|
_shtab_ahriman() {
|
||||||
|
local completing_word="${COMP_WORDS[COMP_CWORD]}"
|
||||||
|
local previous_word="${COMP_WORDS[COMP_CWORD-1]}"
|
||||||
|
local completed_positional_actions
|
||||||
|
local current_action
|
||||||
|
local current_action_args_start_index
|
||||||
|
local current_action_choices
|
||||||
|
local current_action_compgen
|
||||||
|
local current_action_is_positional
|
||||||
|
local current_action_nargs
|
||||||
|
local current_option_strings
|
||||||
|
local sub_parsers
|
||||||
|
COMPREPLY=()
|
||||||
|
|
||||||
|
local prefix=_shtab_ahriman
|
||||||
|
local word_index=0
|
||||||
|
local pos_only=0 # "--" delimeter not encountered yet
|
||||||
|
_set_parser_defaults
|
||||||
|
word_index=1
|
||||||
|
|
||||||
|
# determine what arguments are appropriate for the current state
|
||||||
|
# of the arg parser
|
||||||
|
while [ $word_index -ne $COMP_CWORD ]; do
|
||||||
|
local this_word="${COMP_WORDS[$word_index]}"
|
||||||
|
|
||||||
|
if [[ $pos_only = 1 || " $this_word " != " -- " ]]; then
|
||||||
|
if [[ -n $sub_parsers && " ${sub_parsers[@]} " == *" ${this_word} "* ]]; then
|
||||||
|
# valid subcommand: add it to the prefix & reset the current action
|
||||||
|
prefix="${prefix}_$(_shtab_replace_nonword $this_word)"
|
||||||
|
_set_parser_defaults
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ " ${current_option_strings[@]} " == *" ${this_word} "* ]]; then
|
||||||
|
# a new action should be acquired (due to recognised option string or
|
||||||
|
# no more input expected from current action);
|
||||||
|
# the next positional action can fill in here
|
||||||
|
_set_new_action $this_word false
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$current_action_nargs" != "*" ]] && \
|
||||||
|
[[ "$current_action_nargs" != "+" ]] && \
|
||||||
|
[[ "$current_action_nargs" != "?" ]] && \
|
||||||
|
[[ "$current_action_nargs" != *"..." ]] && \
|
||||||
|
(( $word_index + 1 - $current_action_args_start_index - $pos_only >= \
|
||||||
|
$current_action_nargs )); then
|
||||||
|
$current_action_is_positional && let "completed_positional_actions += 1"
|
||||||
|
_set_new_action "pos_${completed_positional_actions}" true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
pos_only=1 # "--" delimeter encountered
|
||||||
|
fi
|
||||||
|
|
||||||
|
let "word_index+=1"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Generate the completions
|
||||||
|
|
||||||
|
if [[ $pos_only = 0 && "${completing_word}" == -* ]]; then
|
||||||
|
# optional argument started: use option strings
|
||||||
|
COMPREPLY=( $(compgen -W "${current_option_strings[*]}" -- "${completing_word}") )
|
||||||
|
elif [[ "${previous_word}" == ">" || "${previous_word}" == ">>" ||
|
||||||
|
"${previous_word}" =~ ^[12]">" || "${previous_word}" =~ ^[12]">>" ]]; then
|
||||||
|
# handle redirection operators
|
||||||
|
COMPREPLY=( $(compgen -f -- "${completing_word}") )
|
||||||
|
else
|
||||||
|
# use choices & compgen
|
||||||
|
local IFS=$'\n' # items may contain spaces, so delimit using newline
|
||||||
|
COMPREPLY=( $([ -n "${current_action_compgen}" ] \
|
||||||
|
&& "${current_action_compgen}" "${completing_word}") )
|
||||||
|
unset IFS
|
||||||
|
COMPREPLY+=( $(compgen -W "${current_action_choices[*]}" -- "${completing_word}") )
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
complete -o filenames -F _shtab_ahriman ahriman
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "ahriman-core"
|
||||||
|
description = "ArcH linux ReposItory MANager, core package"
|
||||||
|
readme = "../README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
license = {file = "../COPYING"}
|
||||||
|
authors = [
|
||||||
|
{name = "ahriman team"},
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"bcrypt",
|
||||||
|
"filelock",
|
||||||
|
"inflection",
|
||||||
|
"pyelftools",
|
||||||
|
"requests",
|
||||||
|
]
|
||||||
|
dynamic = ["version"]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
journald = [
|
||||||
|
"systemd-python",
|
||||||
|
]
|
||||||
|
# FIXME technically this dependency is required, but in some cases we do not have access to
|
||||||
|
# the libalpm which is required in order to install the package. Thus in case if we do not
|
||||||
|
# really need to run the application we can move it to "optional" dependencies
|
||||||
|
pacman = [
|
||||||
|
"pyalpm",
|
||||||
|
]
|
||||||
|
reports = [
|
||||||
|
"Jinja2",
|
||||||
|
]
|
||||||
|
s3 = [
|
||||||
|
"boto3",
|
||||||
|
]
|
||||||
|
shell = [
|
||||||
|
"IPython",
|
||||||
|
]
|
||||||
|
stats = [
|
||||||
|
"matplotlib",
|
||||||
|
]
|
||||||
|
unixsocket = [
|
||||||
|
"requests-unixsocket2",
|
||||||
|
]
|
||||||
|
validator = [
|
||||||
|
"cerberus",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
ahriman = "ahriman.application.ahriman:run"
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Documentation = "https://ahriman.readthedocs.io/"
|
||||||
|
Repository = "https://github.com/arcan1s/ahriman"
|
||||||
|
Changelog = "https://github.com/arcan1s/ahriman/releases"
|
||||||
|
|
||||||
|
[tool.hatch.version]
|
||||||
|
path = "src/ahriman/__init__.py"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/ahriman"]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel.shared-data]
|
||||||
|
"package/lib" = "lib"
|
||||||
|
"package/share" = "share"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
__version__ = "2.20.0"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import ahriman.application.handlers
|
||||||
|
|
||||||
|
from ahriman import __version__
|
||||||
|
from ahriman.application.handlers.handler import Handler
|
||||||
|
from ahriman.application.help_formatter import _HelpFormatter
|
||||||
|
from ahriman.core.module_loader import implementations
|
||||||
|
from ahriman.core.utils import enum_values
|
||||||
|
from ahriman.models.log_handler import LogHandler
|
||||||
|
|
||||||
|
|
||||||
|
__all__: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
def _parser() -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
command line parser generator
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: command line parser for the application
|
||||||
|
"""
|
||||||
|
parser = argparse.ArgumentParser(prog="ahriman", description="ArcH linux ReposItory MANager",
|
||||||
|
epilog="""
|
||||||
|
Quick setup command (replace repository name, architecture and packager as needed):
|
||||||
|
|
||||||
|
>>> ahriman -a x86_64 -r aur service-setup --packager "ahriman bot <ahriman@example.com>"
|
||||||
|
|
||||||
|
Add new package from AUR:
|
||||||
|
|
||||||
|
>>> ahriman package-add ahriman --now
|
||||||
|
|
||||||
|
Check for updates and build out-of-dated packages (add ``--dry-run`` to build it later):
|
||||||
|
|
||||||
|
>>> ahriman repo-update
|
||||||
|
|
||||||
|
Remove package from the repository:
|
||||||
|
|
||||||
|
>>> ahriman package-remove ahriman
|
||||||
|
|
||||||
|
Start web service (requires additional configuration):
|
||||||
|
|
||||||
|
>>> ahriman web
|
||||||
|
""",
|
||||||
|
fromfile_prefix_chars="@", formatter_class=_HelpFormatter)
|
||||||
|
parser.add_argument("-a", "--architecture", help="filter by target architecture")
|
||||||
|
parser.add_argument("-c", "--configuration", help="configuration path", type=Path,
|
||||||
|
default=Path("/") / "etc" / "ahriman.ini")
|
||||||
|
parser.add_argument("--force", help="force run, remove file lock", action="store_true")
|
||||||
|
parser.add_argument("-l", "--lock", help="lock file", type=Path, default=Path("ahriman.pid"))
|
||||||
|
parser.add_argument("--log-handler", help="explicit log handler specification. If none set, the handler will be "
|
||||||
|
"guessed from environment",
|
||||||
|
type=LogHandler, choices=enum_values(LogHandler))
|
||||||
|
parser.add_argument("-q", "--quiet", help="force disable any logging", action="store_true")
|
||||||
|
parser.add_argument("--report", help="force enable or disable reporting to web service",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("-r", "--repository", help="filter by target repository")
|
||||||
|
# special secret argument for systemd unit. The issue is that systemd doesn't allow multiple arguments to template
|
||||||
|
# name. This parameter accepts [[arch]-repo] in order to keep backward compatibility
|
||||||
|
parser.add_argument("--repository-id", help=argparse.SUPPRESS)
|
||||||
|
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user. Some actions might be unavailable",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("-V", "--version", action="version", version=__version__)
|
||||||
|
parser.add_argument("--wait-timeout", help="wait for lock to be free. Negative value will lead to "
|
||||||
|
"immediate application run even if there is lock file. "
|
||||||
|
"In case of zero value, the application will wait infinitely",
|
||||||
|
type=int, default=-1)
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command")
|
||||||
|
|
||||||
|
for handler in implementations(ahriman.application.handlers, Handler):
|
||||||
|
for subparser_parser in handler.arguments:
|
||||||
|
subparser = subparser_parser(subparsers)
|
||||||
|
subparser.formatter_class = _HelpFormatter
|
||||||
|
subparser.set_defaults(handler=handler, parser=_parser)
|
||||||
|
|
||||||
|
# sort actions alphabetically in both choices and help message
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
subparsers._choices_actions = sorted(subparsers._choices_actions, key=lambda action: action.dest)
|
||||||
|
subparsers.choices = dict(sorted(subparsers.choices.items()))
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def run() -> int:
|
||||||
|
"""
|
||||||
|
run application instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: application status code
|
||||||
|
"""
|
||||||
|
parser = _parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command is None: # in case of empty command we would like to print help message
|
||||||
|
parser.exit(status=2, message=parser.format_help())
|
||||||
|
|
||||||
|
handler: Handler = args.handler
|
||||||
|
return handler.execute(args)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from ahriman.application.application.application import Application
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from collections.abc import Callable, Iterable
|
||||||
|
|
||||||
|
from ahriman.application.application.application_packages import ApplicationPackages
|
||||||
|
from ahriman.application.application.application_repository import ApplicationRepository
|
||||||
|
from ahriman.core.formatters import UpdatePrinter
|
||||||
|
from ahriman.core.tree import Tree
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.result import Result
|
||||||
|
|
||||||
|
|
||||||
|
class Application(ApplicationPackages, ApplicationRepository):
|
||||||
|
"""
|
||||||
|
base application class
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
This class groups :class:`ahriman.core.repository.repository.Repository` methods into specific method which
|
||||||
|
process all supposed actions caused by underlying action. E.g.::
|
||||||
|
|
||||||
|
>>> from ahriman.core.configuration import Configuration
|
||||||
|
>>> from ahriman.models.package_source import PackageSource
|
||||||
|
>>> from ahriman.models.repository_id import RepositoryId
|
||||||
|
>>>
|
||||||
|
>>> configuration = Configuration()
|
||||||
|
>>> application = Application(RepositoryId("x86_64", "x86_64"), configuration, report=True)
|
||||||
|
>>> # add packages to build queue
|
||||||
|
>>> application.add(["ahriman"], PackageSource.AUR)
|
||||||
|
>>>
|
||||||
|
>>> # check for updates
|
||||||
|
>>> updates = application.updates([], aur=True, local=True, manual=True, vcs=True)
|
||||||
|
>>> # updates for specified packages
|
||||||
|
>>> application.update(updates)
|
||||||
|
|
||||||
|
In case if specific actions or their order are required, the direct access to
|
||||||
|
:class:`ahriman.core.repository.repository.Repository` must be used instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _known_packages(self) -> set[str]:
|
||||||
|
"""
|
||||||
|
load packages from repository and pacman repositories
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
set[str]: list of known packages
|
||||||
|
"""
|
||||||
|
known_packages: set[str] = set()
|
||||||
|
# local set
|
||||||
|
# this action is not really needed in case if ``alpm.use_ahriman_cache`` set to yes, because pacman
|
||||||
|
# will eventually contain all the local packages
|
||||||
|
for base in self.repository.packages():
|
||||||
|
for package, properties in base.packages.items():
|
||||||
|
known_packages.add(package)
|
||||||
|
known_packages.update(properties.provides)
|
||||||
|
# known pacman databases
|
||||||
|
known_packages.update(self.repository.pacman.packages())
|
||||||
|
return known_packages
|
||||||
|
|
||||||
|
def on_result(self, result: Result) -> None:
|
||||||
|
"""
|
||||||
|
generate report and sync to remote server
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result(Result): build result
|
||||||
|
"""
|
||||||
|
packages = self.repository.packages()
|
||||||
|
self.repository.triggers.on_result(result, packages)
|
||||||
|
|
||||||
|
def on_start(self) -> None:
|
||||||
|
"""
|
||||||
|
run triggers on start of the application
|
||||||
|
"""
|
||||||
|
self.repository.triggers.on_start()
|
||||||
|
|
||||||
|
def on_stop(self) -> None:
|
||||||
|
"""
|
||||||
|
run triggers on stop of the application. Note, however, that in most cases this method should not be called
|
||||||
|
directly as it will be called after on_start action
|
||||||
|
"""
|
||||||
|
self.repository.triggers.on_stop()
|
||||||
|
|
||||||
|
def print_updates(self, packages: list[Package], *, log_fn: Callable[[str], None]) -> None:
|
||||||
|
"""
|
||||||
|
print list of packages to be built. This method will build dependency tree and print updates accordingly
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packages(list[Package]): package list to be printed
|
||||||
|
log_fn(Callable[[str], None]): logger function to log updates
|
||||||
|
"""
|
||||||
|
local_versions = {package.base: package.version for package in self.repository.packages()}
|
||||||
|
|
||||||
|
tree = Tree.resolve(packages)
|
||||||
|
for level in tree:
|
||||||
|
for package in level:
|
||||||
|
UpdatePrinter(package, local_versions.get(package.base))(verbose=True, log_fn=log_fn, separator=" -> ")
|
||||||
|
|
||||||
|
def with_dependencies(self, packages: list[Package], *, process_dependencies: bool) -> list[Package]:
|
||||||
|
"""
|
||||||
|
add missing dependencies to list of packages. This will extract known packages, check dependencies of
|
||||||
|
the supplied packages and add packages which are not presented in the list of known packages.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packages(list[Package]): list of source packages of which dependencies have to be processed
|
||||||
|
process_dependencies(bool): if set to ``False``, dependencies will not be processed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Package]: updated packages list. Packager for dependencies will be copied from the original package
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
In the most cases, in order to avoid build failure, it is required to add missing packages, which can be
|
||||||
|
done by calling::
|
||||||
|
|
||||||
|
>>> application = ...
|
||||||
|
>>> packages = application.with_dependencies(packages, process_dependencies=True)
|
||||||
|
>>> application.print_updates(packages, log_fn=print)
|
||||||
|
"""
|
||||||
|
if not process_dependencies or not packages:
|
||||||
|
return packages
|
||||||
|
|
||||||
|
def missing_dependencies(sources: Iterable[Package]) -> dict[str, str | None]:
|
||||||
|
# append list of known packages with packages which are in current sources
|
||||||
|
satisfied_packages = known_packages | {
|
||||||
|
single
|
||||||
|
for source in sources
|
||||||
|
for single in source.packages_full
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dependency: source.packager
|
||||||
|
for source in sources
|
||||||
|
for dependency in source.depends_build
|
||||||
|
if dependency not in satisfied_packages
|
||||||
|
}
|
||||||
|
|
||||||
|
def new_packages(root: Package) -> dict[str, Package]:
|
||||||
|
portion = {root.base: root}
|
||||||
|
while missing := missing_dependencies(portion.values()):
|
||||||
|
for package_name, packager in missing.items():
|
||||||
|
if (source_dir := self.repository.paths.cache_for(package_name)).is_dir():
|
||||||
|
# there is local cache, load package from it
|
||||||
|
leaf = Package.from_build(source_dir, self.repository.repository_id.architecture, packager)
|
||||||
|
else:
|
||||||
|
leaf = Package.from_aur(package_name, packager, include_provides=True)
|
||||||
|
portion[leaf.base] = leaf
|
||||||
|
|
||||||
|
# register package in the database
|
||||||
|
self.reporter.set_unknown(leaf)
|
||||||
|
|
||||||
|
return portion
|
||||||
|
|
||||||
|
known_packages = self._known_packages()
|
||||||
|
with_dependencies: dict[str, Package] = {}
|
||||||
|
for package in packages:
|
||||||
|
with self.in_package_context(package.base, package.version): # use the same context for the logger
|
||||||
|
try:
|
||||||
|
with_dependencies |= new_packages(package)
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("could not process dependencies of %s, skip the package", package.base)
|
||||||
|
|
||||||
|
return list(with_dependencies.values())
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import requests
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ahriman.application.application.application_properties import ApplicationProperties
|
||||||
|
from ahriman.core.build_tools.sources import Sources
|
||||||
|
from ahriman.core.exceptions import UnknownPackageError
|
||||||
|
from ahriman.core.utils import package_like
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.package_source import PackageSource
|
||||||
|
from ahriman.models.result import Result
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationPackages(ApplicationProperties):
|
||||||
|
"""
|
||||||
|
package control class
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _add_archive(self, source: str, *_: Any) -> None:
|
||||||
|
"""
|
||||||
|
add package from archive
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source(str): path to package archive
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UnknownPackageError: if specified path doesn't exist
|
||||||
|
"""
|
||||||
|
local_path = Path(source)
|
||||||
|
if not local_path.is_file():
|
||||||
|
raise UnknownPackageError(source)
|
||||||
|
|
||||||
|
dst = self.repository.paths.packages / local_path.name
|
||||||
|
shutil.copy(local_path, dst)
|
||||||
|
|
||||||
|
def _add_aur(self, source: str, username: str | None) -> None:
|
||||||
|
"""
|
||||||
|
add package from AUR
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source(str): package base name
|
||||||
|
username(str | None): optional override of username for build process
|
||||||
|
"""
|
||||||
|
package = Package.from_aur(source, username)
|
||||||
|
self.database.build_queue_insert(package)
|
||||||
|
self.reporter.set_unknown(package)
|
||||||
|
|
||||||
|
def _add_directory(self, source: str, *_: Any) -> None:
|
||||||
|
"""
|
||||||
|
add packages from directory
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source(str): path to local directory
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UnknownPackageError: if specified package is unknown or doesn't exist
|
||||||
|
"""
|
||||||
|
local_dir = Path(source)
|
||||||
|
if not local_dir.is_dir():
|
||||||
|
raise UnknownPackageError(source)
|
||||||
|
|
||||||
|
for full_path in filter(package_like, local_dir.iterdir()):
|
||||||
|
self._add_archive(str(full_path))
|
||||||
|
|
||||||
|
def _add_local(self, source: str, username: str | None) -> None:
|
||||||
|
"""
|
||||||
|
add package from local PKGBUILDs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source(str): path to directory with local source files
|
||||||
|
username(str | None): optional override of username for build process
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UnknownPackageError: if specified package is unknown or doesn't exist
|
||||||
|
"""
|
||||||
|
if (source_dir := Path(source)).is_dir():
|
||||||
|
package = Package.from_build(source_dir, self.architecture, username)
|
||||||
|
cache_dir = self.repository.paths.cache_for(package.base)
|
||||||
|
shutil.copytree(source_dir, cache_dir, dirs_exist_ok=True) # copy package to store in caches
|
||||||
|
Sources.init(cache_dir) # we need to run init command in directory where we do have permissions
|
||||||
|
elif (source_dir := self.repository.paths.cache_for(source)).is_dir():
|
||||||
|
package = Package.from_build(source_dir, self.architecture, username)
|
||||||
|
else:
|
||||||
|
raise UnknownPackageError(source)
|
||||||
|
|
||||||
|
self.database.build_queue_insert(package)
|
||||||
|
|
||||||
|
def _add_remote(self, source: str, *_: Any) -> None:
|
||||||
|
"""
|
||||||
|
add package from remote sources (e.g. HTTP)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source(str): remote URL of the package archive
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UnknownPackageError: if specified package is unknown or doesn't exist
|
||||||
|
"""
|
||||||
|
# timeout=None to suppress pylint warns. Also suppress bandit warnings
|
||||||
|
try:
|
||||||
|
response = requests.get(source, stream=True, timeout=None) # nosec
|
||||||
|
response.raise_for_status()
|
||||||
|
except Exception:
|
||||||
|
raise UnknownPackageError(source)
|
||||||
|
|
||||||
|
dst = self.repository.paths.packages / Path(source).name # URL is path, is not it?
|
||||||
|
with dst.open("wb") as local_file:
|
||||||
|
for chunk in response.iter_content(chunk_size=1024):
|
||||||
|
local_file.write(chunk)
|
||||||
|
|
||||||
|
def _add_repository(self, source: str, username: str | None) -> None:
|
||||||
|
"""
|
||||||
|
add package from official repository
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source(str): package base name
|
||||||
|
username(str | None): optional override of username for build process
|
||||||
|
"""
|
||||||
|
package = Package.from_official(source, self.repository.pacman, username)
|
||||||
|
self.database.build_queue_insert(package)
|
||||||
|
self.reporter.set_unknown(package)
|
||||||
|
|
||||||
|
def add(self, packages: Iterable[str], source: PackageSource, username: str | None = None) -> None:
|
||||||
|
"""
|
||||||
|
add packages for the next build
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packages(Iterable[str]): list of package bases to add
|
||||||
|
source(PackageSource): package source to add
|
||||||
|
username(str | None, optional): optional override of username for build process (Default value = None)
|
||||||
|
"""
|
||||||
|
for package in packages:
|
||||||
|
resolved_source = source.resolve(package, self.repository.paths)
|
||||||
|
fn = getattr(self, f"_add_{resolved_source.value}")
|
||||||
|
fn(package, username)
|
||||||
|
|
||||||
|
def on_result(self, result: Result) -> None:
|
||||||
|
"""
|
||||||
|
generate report and sync to remote server
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result(Result): build result
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: not implemented method
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def remove(self, packages: Iterable[str]) -> Result:
|
||||||
|
"""
|
||||||
|
remove packages from repository
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packages(Iterable[str]): list of packages (either base or name) to remove
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result: removal result
|
||||||
|
"""
|
||||||
|
result = self.repository.process_remove(packages)
|
||||||
|
self.on_result(result)
|
||||||
|
return result
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.database import SQLite
|
||||||
|
from ahriman.core.log import LazyLogging
|
||||||
|
from ahriman.core.repository import Repository
|
||||||
|
from ahriman.core.status import Client
|
||||||
|
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationProperties(LazyLogging):
|
||||||
|
"""
|
||||||
|
application base properties class
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
database(SQLite): database instance
|
||||||
|
repository(Repository): repository instance
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, repository_id: RepositoryId, configuration: Configuration, *, report: bool,
|
||||||
|
refresh_pacman_database: PacmanSynchronization = PacmanSynchronization.Disabled) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
refresh_pacman_database(PacmanSynchronization, optional): pacman database synchronization level
|
||||||
|
(Default value = PacmanSynchronization.Disabled)
|
||||||
|
"""
|
||||||
|
self.configuration = configuration
|
||||||
|
self.repository_id = repository_id
|
||||||
|
self.database = SQLite.load(configuration)
|
||||||
|
self.repository = Repository.load(repository_id, configuration, self.database, report=report,
|
||||||
|
refresh_pacman_database=refresh_pacman_database)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def architecture(self) -> str:
|
||||||
|
"""
|
||||||
|
repository architecture for backward compatibility
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: repository architecture
|
||||||
|
"""
|
||||||
|
return self.repository_id.architecture
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reporter(self) -> Client:
|
||||||
|
"""
|
||||||
|
instance of the web/database client
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Client: repository reposter
|
||||||
|
"""
|
||||||
|
return self.repository.reporter
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
from ahriman.application.application.application_properties import ApplicationProperties
|
||||||
|
from ahriman.application.application.workers import Updater
|
||||||
|
from ahriman.core.build_tools.sources import Sources
|
||||||
|
from ahriman.core.exceptions import UnknownPackageError
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.packagers import Packagers
|
||||||
|
from ahriman.models.result import Result
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationRepository(ApplicationProperties):
|
||||||
|
"""
|
||||||
|
repository control class
|
||||||
|
"""
|
||||||
|
|
||||||
|
def changes(self, packages: Iterable[Package]) -> None:
|
||||||
|
"""
|
||||||
|
generate and update package changes
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packages(Iterable[Package]): list of packages to retrieve changes
|
||||||
|
"""
|
||||||
|
for package in packages:
|
||||||
|
last_commit_sha = self.reporter.package_changes_get(package.base).last_commit_sha
|
||||||
|
if last_commit_sha is None:
|
||||||
|
continue # skip check in case if we can't calculate diff
|
||||||
|
|
||||||
|
if (changes := self.repository.package_changes(package, last_commit_sha)) is not None:
|
||||||
|
self.reporter.package_changes_update(package.base, changes)
|
||||||
|
|
||||||
|
def clean(self, *, cache: bool, chroot: bool, manual: bool, packages: bool, pacman: bool) -> None:
|
||||||
|
"""
|
||||||
|
run all clean methods. Warning: some functions might not be available for non-root user
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cache(bool): clear directory with package caches
|
||||||
|
chroot(bool): clear build chroot
|
||||||
|
manual(bool): clear directory with manually added packages' bases
|
||||||
|
packages(bool): clear directory with built packages
|
||||||
|
pacman(bool): clear directory with pacman databases
|
||||||
|
"""
|
||||||
|
if cache:
|
||||||
|
self.repository.clear_cache()
|
||||||
|
if chroot:
|
||||||
|
self.repository.clear_chroot()
|
||||||
|
if manual:
|
||||||
|
self.repository.clear_queue()
|
||||||
|
if packages:
|
||||||
|
self.repository.clear_packages()
|
||||||
|
if pacman:
|
||||||
|
self.repository.clear_pacman()
|
||||||
|
|
||||||
|
def on_result(self, result: Result) -> None:
|
||||||
|
"""
|
||||||
|
generate report and sync to remote server
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result(Result): build result
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: not implemented method
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def sign(self, packages: Iterable[str]) -> None:
|
||||||
|
"""
|
||||||
|
sign packages and repository
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packages(Iterable[str]): only sign specified packages
|
||||||
|
"""
|
||||||
|
# copy to prebuilt directory
|
||||||
|
for package in self.repository.packages(packages):
|
||||||
|
for archive in package.packages.values():
|
||||||
|
if archive.filepath is None:
|
||||||
|
self.logger.warning("filepath is empty for %s", package.base)
|
||||||
|
continue # avoid mypy warning
|
||||||
|
self.repository.sign.process_sign_package(archive.filepath, None)
|
||||||
|
# sign repository database if set
|
||||||
|
self.repository.sign.process_sign_repository(self.repository.repo.repo_path)
|
||||||
|
# process triggers
|
||||||
|
self.on_result(Result())
|
||||||
|
|
||||||
|
def unknown(self) -> list[str]:
|
||||||
|
"""
|
||||||
|
get packages which were not found in AUR
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]: unknown package archive list
|
||||||
|
"""
|
||||||
|
def has_local(probe: Package) -> bool:
|
||||||
|
cache_dir = self.repository.paths.cache_for(probe.base)
|
||||||
|
return cache_dir.is_dir() and not Sources.has_remotes(cache_dir)
|
||||||
|
|
||||||
|
def unknown_aur(probe: Package) -> list[str]:
|
||||||
|
packages: list[str] = []
|
||||||
|
for single in probe.packages:
|
||||||
|
try:
|
||||||
|
_ = Package.from_aur(single, None)
|
||||||
|
except UnknownPackageError:
|
||||||
|
packages.append(single)
|
||||||
|
return packages
|
||||||
|
|
||||||
|
def unknown_local(probe: Package) -> list[str]:
|
||||||
|
cache_dir = self.repository.paths.cache_for(probe.base)
|
||||||
|
local = Package.from_build(cache_dir, self.architecture, None)
|
||||||
|
packages = set(probe.packages.keys()).difference(local.packages.keys())
|
||||||
|
return list(packages)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for package in self.repository.packages():
|
||||||
|
if has_local(package):
|
||||||
|
result.extend(unknown_local(package)) # there is local package
|
||||||
|
else:
|
||||||
|
result.extend(unknown_aur(package)) # local package not found
|
||||||
|
return result
|
||||||
|
|
||||||
|
def update(self, updates: Iterable[Package], packagers: Packagers | None = None, *,
|
||||||
|
bump_pkgrel: bool = False) -> Result:
|
||||||
|
"""
|
||||||
|
run package updates. This method will separate update in the several steps:
|
||||||
|
|
||||||
|
#. Check already built packages.
|
||||||
|
#. Construct builder instance.
|
||||||
|
#. Delegate build process to the builder instance (either remote or local).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
updates(Iterable[Package]): list of packages to update
|
||||||
|
packagers(Packagers | None, optional): optional override of username for build process
|
||||||
|
(Default value = None)
|
||||||
|
bump_pkgrel(bool, optional): bump pkgrel in case of local version conflict (Default value = False)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result: update result
|
||||||
|
"""
|
||||||
|
result = Result()
|
||||||
|
|
||||||
|
# process already built packages if any
|
||||||
|
if built_packages := self.repository.packages_built(): # speedup a bit
|
||||||
|
build_result = self.repository.process_update(built_packages, packagers)
|
||||||
|
self.on_result(build_result)
|
||||||
|
result.merge(build_result)
|
||||||
|
|
||||||
|
# filter packages which were prebuilt
|
||||||
|
succeeded = {package.base for package in build_result.success}
|
||||||
|
updates = [package for package in updates if package.base not in succeeded]
|
||||||
|
|
||||||
|
builder = Updater.load(self.repository_id, self.configuration, self.repository)
|
||||||
|
|
||||||
|
# ok so for now we split all packages into chunks and process each chunk accordingly
|
||||||
|
partitions = builder.partition(updates)
|
||||||
|
for num, partition in enumerate(partitions):
|
||||||
|
self.logger.info("processing chunk #%i %s", num, [package.base for package in partition])
|
||||||
|
build_result = builder.update(partition, packagers, bump_pkgrel=bump_pkgrel)
|
||||||
|
self.on_result(build_result)
|
||||||
|
result.merge(build_result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def updates(self, filter_packages: Iterable[str], *,
|
||||||
|
aur: bool, local: bool, manual: bool, vcs: bool, check_files: bool) -> list[Package]:
|
||||||
|
"""
|
||||||
|
get list of packages to run update process
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filter_packages(Iterable[str]): do not check every package just specified in the list
|
||||||
|
aur(bool): enable or disable checking for AUR updates
|
||||||
|
local(bool): enable or disable checking of local packages for updates
|
||||||
|
manual(bool): include or exclude manual updates
|
||||||
|
vcs(bool): enable or disable checking of VCS packages
|
||||||
|
check_files(bool): check for broken dependencies
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Package]: list of out-of-dated packages
|
||||||
|
"""
|
||||||
|
updates = {}
|
||||||
|
|
||||||
|
# always add already built packages, because they will be always added
|
||||||
|
updates.update({
|
||||||
|
package.base: package
|
||||||
|
for package in self.repository.load_archives(self.repository.packages_built())
|
||||||
|
})
|
||||||
|
|
||||||
|
if aur:
|
||||||
|
updates.update({package.base: package for package in self.repository.updates_aur(filter_packages, vcs=vcs)})
|
||||||
|
if local:
|
||||||
|
updates.update({package.base: package for package in self.repository.updates_local(vcs=vcs)})
|
||||||
|
if manual:
|
||||||
|
updates.update({package.base: package for package in self.repository.updates_manual()})
|
||||||
|
if check_files:
|
||||||
|
updates.update({package.base: package for package in self.repository.updates_dependencies(filter_packages)})
|
||||||
|
|
||||||
|
return [package for _, package in sorted(updates.items())]
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import time
|
||||||
|
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.core.tree import Tree
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatesIterator(Iterator[list[str] | None]):
|
||||||
|
"""
|
||||||
|
class-helper for iteration over packages to check for updates. It yields list of packages which were not yet
|
||||||
|
updated
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
application(Application): application instance
|
||||||
|
interval(int): predefined interval for updates. The updates will be split into chunks in the way in which all
|
||||||
|
packages will be updated in the specified interval
|
||||||
|
updated_packages(set[str]): list of packages which have been already updated
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Typical usage of this class is something like:
|
||||||
|
|
||||||
|
>>> application = ...
|
||||||
|
>>> iterator = UpdatesIterator(application, None)
|
||||||
|
>>>
|
||||||
|
>>> for updates in iterator:
|
||||||
|
>>> print(updates)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, application: Application, interval: int) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
interval(int): predefined interval for updates
|
||||||
|
"""
|
||||||
|
self.application = application
|
||||||
|
self.interval = interval
|
||||||
|
|
||||||
|
self.updated_packages: set[str] = set()
|
||||||
|
|
||||||
|
def select_packages(self) -> tuple[list[str] | None, int]:
|
||||||
|
"""
|
||||||
|
select next packages partition for updates
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[list[str] | None, int]: packages partition for updates if any and total amount of partitions.
|
||||||
|
"""
|
||||||
|
packages = self.application.repository.packages()
|
||||||
|
if not packages: # empty repository case
|
||||||
|
return None, 1
|
||||||
|
|
||||||
|
# split packages to the maximal available amount of chunks
|
||||||
|
partitions = Tree.partition(packages, count=len(packages))
|
||||||
|
frequency = len(partitions) # must be always not-empty
|
||||||
|
|
||||||
|
for partition in partitions:
|
||||||
|
bases = [package.base for package in partition]
|
||||||
|
# check if all packages from this partition have been already updated
|
||||||
|
if self.updated_packages.issuperset(bases):
|
||||||
|
continue
|
||||||
|
# there are packages which were not checked yet, return them
|
||||||
|
return bases, frequency
|
||||||
|
|
||||||
|
# in this case there is nothing to update or repository is empty
|
||||||
|
self.updated_packages.clear()
|
||||||
|
|
||||||
|
# extract bases from the first partition and return them
|
||||||
|
bases = [package.base for package in next(iter(partitions))]
|
||||||
|
return bases, frequency
|
||||||
|
|
||||||
|
def __iter__(self) -> Self:
|
||||||
|
"""
|
||||||
|
base iterator method
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Self: iterator instance
|
||||||
|
"""
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self) -> list[str] | None:
|
||||||
|
"""
|
||||||
|
retrieve next element in the iterator. This method will delay result for the amount of time equals
|
||||||
|
:attr:`interval` divided by the amount of chunks
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str] | None: next packages chunk to be updated. ``None`` means no updates
|
||||||
|
"""
|
||||||
|
to_update, frequency = self.select_packages()
|
||||||
|
if to_update is not None:
|
||||||
|
# update cached built packages
|
||||||
|
self.updated_packages.update(to_update)
|
||||||
|
|
||||||
|
# wait for update before emit
|
||||||
|
time.sleep(self.interval / frequency)
|
||||||
|
|
||||||
|
return to_update
|
||||||
|
|
||||||
|
|
||||||
|
class FixedUpdatesIterator(UpdatesIterator):
|
||||||
|
"""
|
||||||
|
implementation of the :class:`UpdatesIterator` which always emits empty list, which is the same as update all
|
||||||
|
"""
|
||||||
|
|
||||||
|
def select_packages(self) -> tuple[list[str] | None, int]:
|
||||||
|
"""
|
||||||
|
select next packages partition for updates
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[list[str] | None, int]: packages partition for updates if any and total amount of partitions.
|
||||||
|
"""
|
||||||
|
return [], 1
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from ahriman.application.application.workers.updater import Updater
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
from ahriman.application.application.workers.updater import Updater
|
||||||
|
from ahriman.core.repository import Repository
|
||||||
|
from ahriman.core.tree import Tree
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.packagers import Packagers
|
||||||
|
from ahriman.models.result import Result
|
||||||
|
|
||||||
|
|
||||||
|
class LocalUpdater(Updater):
|
||||||
|
"""
|
||||||
|
local build process implementation
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
repository(Repository): repository instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, repository: Repository) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
repository(Repository): repository instance
|
||||||
|
"""
|
||||||
|
self.repository = repository
|
||||||
|
|
||||||
|
def partition(self, packages: Iterable[Package]) -> list[list[Package]]:
|
||||||
|
"""
|
||||||
|
split packages into partitions to be processed by this worker
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packages(Iterable[Package]): list of packages to partition
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[list[Package]]: packages partitioned by this worker type
|
||||||
|
"""
|
||||||
|
return Tree.resolve(packages)
|
||||||
|
|
||||||
|
def update(self, updates: Iterable[Package], packagers: Packagers | None = None, *,
|
||||||
|
bump_pkgrel: bool = False) -> Result:
|
||||||
|
"""
|
||||||
|
run package updates
|
||||||
|
|
||||||
|
Args:
|
||||||
|
updates(Iterable[Package]): list of packages to update
|
||||||
|
packagers(Packagers | None, optional): optional override of username for build process
|
||||||
|
(Default value = None)
|
||||||
|
bump_pkgrel(bool, optional): bump pkgrel in case of local version conflict (Default value = False)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result: update result
|
||||||
|
"""
|
||||||
|
build_result = self.repository.process_build(updates, packagers, bump_pkgrel=bump_pkgrel)
|
||||||
|
packages = self.repository.packages_built()
|
||||||
|
update_result = self.repository.process_update(packages, packagers)
|
||||||
|
|
||||||
|
return build_result.merge(update_result)
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from collections import deque
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
from ahriman.application.application.workers.updater import Updater
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.http import SyncAhrimanClient
|
||||||
|
from ahriman.core.tree import Tree
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.packagers import Packagers
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
from ahriman.models.result import Result
|
||||||
|
from ahriman.models.worker import Worker
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteUpdater(Updater):
|
||||||
|
"""
|
||||||
|
remote update worker
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
workers(list[Worker]): worker identifiers
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, workers: list[Worker], repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
workers(list[Worker]): worker identifiers
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
"""
|
||||||
|
self.workers = workers
|
||||||
|
self.repository_id = repository_id
|
||||||
|
self.configuration = configuration
|
||||||
|
|
||||||
|
self._clients: deque[tuple[Worker, SyncAhrimanClient]] = deque()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def clients(self) -> dict[Worker, SyncAhrimanClient]:
|
||||||
|
"""
|
||||||
|
extract loaded clients. Note that this method yields only workers which have been already loaded
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[Worker, SyncAhrimanClient]: map of the worker to the related web client
|
||||||
|
"""
|
||||||
|
return dict(self._clients)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _update_url(worker: Worker) -> str:
|
||||||
|
"""
|
||||||
|
get url for updates
|
||||||
|
|
||||||
|
Args:
|
||||||
|
worker(Worker): worker identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: full url for web service to run update process
|
||||||
|
"""
|
||||||
|
return f"{worker.address}/api/v1/service/add"
|
||||||
|
|
||||||
|
def next_worker(self) -> tuple[Worker, SyncAhrimanClient]:
|
||||||
|
"""
|
||||||
|
generate next not-used web client. In case if all clients have been already used, it yields next not used client
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[Worker, SyncAhrimanClient]: worker and constructed client instance for the web
|
||||||
|
"""
|
||||||
|
# check if there is not used yet worker
|
||||||
|
worker = next((worker for worker in self.workers if worker not in self.clients), None)
|
||||||
|
if worker is not None:
|
||||||
|
client = SyncAhrimanClient(self.configuration, "status")
|
||||||
|
client.address = worker.address
|
||||||
|
else:
|
||||||
|
worker, client = self._clients.popleft()
|
||||||
|
|
||||||
|
# register worker in the queue
|
||||||
|
self._clients.append((worker, client))
|
||||||
|
|
||||||
|
return worker, client
|
||||||
|
|
||||||
|
def partition(self, packages: Iterable[Package]) -> list[list[Package]]:
|
||||||
|
"""
|
||||||
|
split packages into partitions to be processed by this worker
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packages(Iterable[Package]): list of packages to partition
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[list[Package]]: packages partitioned by this worker type
|
||||||
|
"""
|
||||||
|
return Tree.partition(packages, count=len(self.workers))
|
||||||
|
|
||||||
|
def update(self, updates: Iterable[Package], packagers: Packagers | None = None, *,
|
||||||
|
bump_pkgrel: bool = False) -> Result:
|
||||||
|
"""
|
||||||
|
run package updates
|
||||||
|
|
||||||
|
Args:
|
||||||
|
updates(Iterable[Package]): list of packages to update
|
||||||
|
packagers(Packagers | None, optional): optional override of username for build process
|
||||||
|
(Default value = None)
|
||||||
|
bump_pkgrel(bool, optional): bump pkgrel in case of local version conflict (Default value = False)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result: update result
|
||||||
|
"""
|
||||||
|
payload = {
|
||||||
|
"increment": False, # force disable increment because it doesn't work yet
|
||||||
|
"packager": packagers.default if packagers is not None else None,
|
||||||
|
"packages": [package.base for package in updates],
|
||||||
|
"patches": [], # might be used later
|
||||||
|
"refresh": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
worker, client = self.next_worker()
|
||||||
|
client.make_request("POST", self._update_url(worker), params=self.repository_id.query(), json=payload)
|
||||||
|
# we don't block here for process
|
||||||
|
|
||||||
|
return Result()
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.log import LazyLogging
|
||||||
|
from ahriman.core.repository import Repository
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.packagers import Packagers
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
from ahriman.models.result import Result
|
||||||
|
from ahriman.models.worker import Worker
|
||||||
|
|
||||||
|
|
||||||
|
class Updater(LazyLogging):
|
||||||
|
"""
|
||||||
|
updater handler interface
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load(repository_id: RepositoryId, configuration: Configuration,
|
||||||
|
repository: Repository, workers: list[Worker] | None = None) -> Updater:
|
||||||
|
"""
|
||||||
|
construct updaters from parameters
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
repository(Repository): repository instance
|
||||||
|
workers(list[Worker] | None, optional): worker identifiers if any (Default value = None)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updater: constructed updater worker
|
||||||
|
"""
|
||||||
|
if workers is None:
|
||||||
|
# no workers set explicitly, try to guess from configuration
|
||||||
|
workers = [Worker(address) for address in configuration.getlist("build", "workers", fallback=[])]
|
||||||
|
|
||||||
|
if workers:
|
||||||
|
# there is something we could use as remote workers
|
||||||
|
from ahriman.application.application.workers.remote_updater import RemoteUpdater
|
||||||
|
return RemoteUpdater(workers, repository_id, configuration)
|
||||||
|
|
||||||
|
# and finally no workers available, just use local service
|
||||||
|
from ahriman.application.application.workers.local_updater import LocalUpdater
|
||||||
|
return LocalUpdater(repository)
|
||||||
|
|
||||||
|
def partition(self, packages: Iterable[Package]) -> list[list[Package]]:
|
||||||
|
"""
|
||||||
|
split packages into partitions to be processed by this worker
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packages(Iterable[Package]): list of packages to partition
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[list[Package]]: packages partitioned by this worker type
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: not implemented method
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def update(self, updates: Iterable[Package], packagers: Packagers | None = None, *,
|
||||||
|
bump_pkgrel: bool = False) -> Result:
|
||||||
|
"""
|
||||||
|
run package updates
|
||||||
|
|
||||||
|
Args:
|
||||||
|
updates(Iterable[Package]): list of packages to update
|
||||||
|
packagers(Packagers | None, optional): optional override of username for build process
|
||||||
|
(Default value = None)
|
||||||
|
bump_pkgrel(bool, optional): bump pkgrel in case of local version conflict (Default value = False)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result: update result
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: not implemented method
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.application.handlers.update import Update
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.utils import enum_values, extract_user
|
||||||
|
from ahriman.models.package_source import PackageSource
|
||||||
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Add(Handler):
|
||||||
|
"""
|
||||||
|
add packages handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
||||||
|
application.on_start()
|
||||||
|
Add.perform_action(application, args)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package addition subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-add", aliases=["add", "package-update"], help="add package",
|
||||||
|
description="add existing or new package to the build queue",
|
||||||
|
epilog="This subcommand should be used for new package addition. "
|
||||||
|
"It also supports flag --now in case if you would like to build "
|
||||||
|
"the package immediately. You can add new package from one of "
|
||||||
|
"supported sources:\n\n"
|
||||||
|
"1. If it is already built package you can specify the path to the archive.\n"
|
||||||
|
"2. You can also add built packages from the directory (e.g. during the "
|
||||||
|
"migration from another repository source).\n"
|
||||||
|
"3. It is also possible to add package from local PKGBUILD, but in this case "
|
||||||
|
"it will be ignored during the next automatic updates.\n"
|
||||||
|
"4. Ahriman supports downloading archives from remote (e.g. HTTP) sources.\n"
|
||||||
|
"5. Finally you can add package from AUR.")
|
||||||
|
parser.add_argument("package", help="package source (base name, path to local files, remote URL)", nargs="+")
|
||||||
|
parser.add_argument("--changes", help="calculate changes from the latest known commit if available",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--dependencies", help="process missing package dependencies",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--increment", help="increment package release (pkgrel) version on duplicate",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
|
||||||
|
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
|
||||||
|
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
|
||||||
|
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
|
||||||
|
parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build",
|
||||||
|
action="append")
|
||||||
|
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
|
||||||
|
"-yy to force refresh even if up to date",
|
||||||
|
action="count", default=False)
|
||||||
|
parser.set_defaults(aur=False, check_files=False, dry_run=False, local=False, manual=True, vcs=False)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def perform_action(application: Application, args: argparse.Namespace) -> None:
|
||||||
|
"""
|
||||||
|
perform add action
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
"""
|
||||||
|
application.add(args.package, args.source, args.username)
|
||||||
|
patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else []
|
||||||
|
for package in args.package: # for each requested package insert patch
|
||||||
|
for patch in patches:
|
||||||
|
application.reporter.package_patches_update(package, patch)
|
||||||
|
|
||||||
|
if not args.now:
|
||||||
|
return
|
||||||
|
Update.perform_action(application, args)
|
||||||
|
|
||||||
|
arguments = [_set_package_add_parser]
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import PackagePrinter
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Archives(Handler):
|
||||||
|
"""
|
||||||
|
package archives handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=True)
|
||||||
|
|
||||||
|
match args.action:
|
||||||
|
case Action.List:
|
||||||
|
archives = application.repository.package_archives(args.package)
|
||||||
|
for package in archives:
|
||||||
|
PackagePrinter(package, BuildStatus(BuildStatusEnum.Success))(verbose=args.info)
|
||||||
|
|
||||||
|
Archives.check_status(args.exit_code, bool(archives))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_archives_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package archives subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-archives", help="list package archive versions",
|
||||||
|
description="list available archive versions for the package")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--info", help="show additional package information",
|
||||||
|
action=argparse.BooleanOptionalAction, default=False)
|
||||||
|
parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_package_archives_parser]
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
import tarfile
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from pwd import getpwuid
|
||||||
|
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.database import SQLite
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Backup(Handler):
|
||||||
|
"""
|
||||||
|
backup packages handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
backup_paths = Backup.get_paths(configuration)
|
||||||
|
with tarfile.open(args.path, mode="w") as archive: # well we don't actually use compression
|
||||||
|
for backup_path in backup_paths:
|
||||||
|
archive.add(backup_path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_repo_backup_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for repository backup subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("repo-backup", help="backup repository data",
|
||||||
|
description="backup repository settings and database")
|
||||||
|
parser.add_argument("path", help="path of the output archive", type=Path)
|
||||||
|
parser.set_defaults(architecture="", lock=None, report=False, repository="", unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_paths(configuration: Configuration) -> set[Path]:
|
||||||
|
"""
|
||||||
|
extract paths to back up
|
||||||
|
|
||||||
|
Args:
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
set[Path]: map of the filesystem paths
|
||||||
|
"""
|
||||||
|
# configuration files
|
||||||
|
root, _ = configuration.check_loaded()
|
||||||
|
paths = set(configuration.includes)
|
||||||
|
paths.add(root)
|
||||||
|
|
||||||
|
# database
|
||||||
|
paths.add(SQLite.database_path(configuration))
|
||||||
|
|
||||||
|
# local caches
|
||||||
|
repository_paths = configuration.repository_paths
|
||||||
|
if repository_paths.cache.is_dir():
|
||||||
|
paths.add(repository_paths.cache)
|
||||||
|
|
||||||
|
# gnupg home with imported keys
|
||||||
|
uid, _ = repository_paths.root_owner
|
||||||
|
system_user = getpwuid(uid)
|
||||||
|
gnupg_home = Path(system_user.pw_dir) / ".gnupg"
|
||||||
|
if gnupg_home.is_dir():
|
||||||
|
paths.add(gnupg_home)
|
||||||
|
|
||||||
|
return paths
|
||||||
|
|
||||||
|
arguments = [_set_repo_backup_parser]
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import ChangesPrinter
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.changes import Changes
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Change(Handler):
|
||||||
|
"""
|
||||||
|
package changes handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
|
|
||||||
|
match args.action:
|
||||||
|
case Action.List:
|
||||||
|
changes = client.package_changes_get(args.package)
|
||||||
|
ChangesPrinter(changes)(verbose=True, separator="")
|
||||||
|
Change.check_status(args.exit_code, changes.changes is not None)
|
||||||
|
case Action.Remove:
|
||||||
|
client.package_changes_update(args.package, Changes())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_changes_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package changes subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-changes", help="get package changes",
|
||||||
|
description="retrieve package changes stored in database",
|
||||||
|
epilog="This command requests package status from the web interface "
|
||||||
|
"if it is available.")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_changes_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package change remove subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-changes-remove", help="remove package changes",
|
||||||
|
description="remove the package changes stored remotely")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.set_defaults(action=Action.Remove, exit_code=False, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_package_changes_parser, _set_package_changes_remove_parser]
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Clean(Handler):
|
||||||
|
"""
|
||||||
|
clean caches handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=report)
|
||||||
|
application.on_start()
|
||||||
|
application.clean(cache=args.cache, chroot=args.chroot, manual=args.manual, packages=args.packages,
|
||||||
|
pacman=args.pacman)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_service_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for repository clean subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("service-clean", aliases=["clean", "repo-clean"], help="clean local caches",
|
||||||
|
description="remove local caches",
|
||||||
|
epilog="The subcommand clears every temporary directories (builds, caches etc). "
|
||||||
|
"Normally you should not run this command manually. Also in case if "
|
||||||
|
"you are going to clear the chroot directories you will need root privileges.")
|
||||||
|
parser.add_argument("--cache", help="clear directory with package caches",
|
||||||
|
action=argparse.BooleanOptionalAction, default=False)
|
||||||
|
parser.add_argument("--chroot", help="clear build chroot", action=argparse.BooleanOptionalAction, default=False)
|
||||||
|
parser.add_argument("--manual", help="clear manually added packages queue",
|
||||||
|
action=argparse.BooleanOptionalAction, default=False)
|
||||||
|
parser.add_argument("--packages", help="clear directory with built packages",
|
||||||
|
action=argparse.BooleanOptionalAction, default=False)
|
||||||
|
parser.add_argument("--pacman", help="clear directory with pacman local database cache",
|
||||||
|
action=argparse.BooleanOptionalAction, default=False)
|
||||||
|
parser.set_defaults(lock=None, quiet=True, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_service_clean_parser]
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.package_source import PackageSource
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Copy(Handler):
|
||||||
|
"""
|
||||||
|
copy packages handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting action
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=report)
|
||||||
|
application.on_start()
|
||||||
|
|
||||||
|
configuration_path, _ = configuration.check_loaded()
|
||||||
|
source_repository_id = RepositoryId(repository_id.architecture, args.source)
|
||||||
|
source_configuration = Configuration.from_path(configuration_path, source_repository_id)
|
||||||
|
source_application = Application(source_repository_id, source_configuration, report=report)
|
||||||
|
|
||||||
|
packages = source_application.repository.packages(args.package)
|
||||||
|
Copy.check_status(args.exit_code, packages)
|
||||||
|
|
||||||
|
for package in packages:
|
||||||
|
Copy.copy_package(package, application, source_application)
|
||||||
|
|
||||||
|
# run update
|
||||||
|
application.update([])
|
||||||
|
|
||||||
|
if args.remove:
|
||||||
|
source_application.remove(args.package)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_copy_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package copy subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-copy", aliases=["copy"], help="copy package from another repository",
|
||||||
|
description="copy package and its metadata from another repository")
|
||||||
|
parser.add_argument("source", help="source repository name")
|
||||||
|
parser.add_argument("package", help="package base", nargs="+")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--remove", help="remove package from the source repository after", action="store_true")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def copy_package(package: Package, application: Application, source_application: Application) -> None:
|
||||||
|
"""
|
||||||
|
copy package ``package`` from source repository to target repository
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package(Package): package to copy
|
||||||
|
application(Application): application instance of the target repository
|
||||||
|
source_application(Application): application instance of the source repository
|
||||||
|
"""
|
||||||
|
# copy files
|
||||||
|
source_paths = [
|
||||||
|
str(source_application.repository.paths.repository / source.filename)
|
||||||
|
for source in package.packages.values()
|
||||||
|
if source.filename is not None
|
||||||
|
]
|
||||||
|
application.add(source_paths, PackageSource.Archive)
|
||||||
|
|
||||||
|
# copy metadata
|
||||||
|
application.reporter.package_changes_update(
|
||||||
|
package.base, source_application.reporter.package_changes_get(package.base)
|
||||||
|
)
|
||||||
|
application.reporter.package_dependencies_update(
|
||||||
|
package.base, source_application.reporter.package_dependencies_get(package.base)
|
||||||
|
)
|
||||||
|
application.reporter.package_update(package, BuildStatusEnum.Pending)
|
||||||
|
|
||||||
|
arguments = [_set_package_copy_parser]
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.application.updates_iterator import FixedUpdatesIterator, UpdatesIterator
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.application.handlers.update import Update
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.utils import extract_user
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Daemon(Handler):
|
||||||
|
"""
|
||||||
|
daemon packages handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
||||||
|
if args.partitions:
|
||||||
|
iterator = UpdatesIterator(application, args.interval)
|
||||||
|
else:
|
||||||
|
iterator = FixedUpdatesIterator(application, args.interval)
|
||||||
|
|
||||||
|
for packages in iterator:
|
||||||
|
if packages is None:
|
||||||
|
continue # nothing to check case
|
||||||
|
|
||||||
|
args.package = packages
|
||||||
|
Update.run(args, repository_id, configuration, report=report)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_repo_daemon_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for daemon subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("repo-daemon", aliases=["daemon"], help="run application as daemon",
|
||||||
|
description="start process which periodically will run update process")
|
||||||
|
parser.add_argument("-i", "--interval", help="interval between runs in seconds", type=int, default=60 * 60 * 12)
|
||||||
|
parser.add_argument("--aur", help="enable or disable checking for AUR updates",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--changes", help="calculate changes from the latest known commit if available. "
|
||||||
|
"Only applicable in dry run mode",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--check-files", help="enable or disable checking of broken dependencies "
|
||||||
|
"(e.g. dynamically linked libraries or modules directories)",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--dependencies", help="process missing package dependencies",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--dry-run", help="just perform check for updates, same as check command",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--increment", help="increment package release (pkgrel) on duplicate",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--local", help="enable or disable checking of local packages for updates",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--manual", help="include or exclude manual updates",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--partitions", help="instead of updating whole repository, split updates into chunks",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
|
||||||
|
parser.add_argument("--vcs", help="fetch actual version of VCS packages",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
|
||||||
|
"-yy to force refresh even if up to date",
|
||||||
|
action="count", default=False)
|
||||||
|
parser.set_defaults(exit_code=False, lock=Path("ahriman-daemon.pid"), package=[])
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_repo_daemon_parser]
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import ConfigurationPathsPrinter, ConfigurationPrinter, StringPrinter
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Dump(Handler):
|
||||||
|
"""
|
||||||
|
dump configuration handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
if args.info:
|
||||||
|
root, _ = configuration.check_loaded()
|
||||||
|
ConfigurationPathsPrinter(root, configuration.includes)(verbose=True, separator=" = ")
|
||||||
|
|
||||||
|
match (args.section, args.key):
|
||||||
|
case None, None: # full configuration
|
||||||
|
dump = configuration.dump()
|
||||||
|
for section, values in sorted(dump.items()):
|
||||||
|
ConfigurationPrinter(section, values)(verbose=not args.secure, separator=" = ")
|
||||||
|
case section, None: # section only
|
||||||
|
values = dict(configuration.items(section)) if configuration.has_section(section) else {}
|
||||||
|
ConfigurationPrinter(section, values)(verbose=not args.secure, separator=" = ")
|
||||||
|
case section, key: # key only
|
||||||
|
value = configuration.get(section, key, fallback="")
|
||||||
|
StringPrinter(value)(verbose=False)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_service_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for config subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("service-config", aliases=["config", "repo-config"], help="dump configuration",
|
||||||
|
description="dump configuration for the specified architecture")
|
||||||
|
parser.add_argument("section", help="filter settings by section", nargs="?")
|
||||||
|
parser.add_argument("key", help="filter settings by key", nargs="?")
|
||||||
|
parser.add_argument("--info", help="show additional information, e.g. configuration files",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--secure", help="hide passwords and secrets from output",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_service_config_parser]
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from multiprocessing import Pool
|
||||||
|
from typing import ClassVar, TypeVar
|
||||||
|
|
||||||
|
from ahriman.application.lock import Lock
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.exceptions import ExitCode, MissingArchitectureError, MultipleArchitecturesError
|
||||||
|
from ahriman.core.log.log_loader import LogLoader
|
||||||
|
from ahriman.core.repository import Explorer
|
||||||
|
from ahriman.core.types import ExplicitBool
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
# this workaround is for several things
|
||||||
|
# firstly python devs don't think that is it error and asking you for workarounds https://bugs.python.org/issue41592
|
||||||
|
# secondly linters don't like when you are importing private members
|
||||||
|
# thirdly new mypy doesn't like _SubParsersAction and thinks it is a template
|
||||||
|
SubParserAction = TypeVar("SubParserAction", bound="argparse._SubParsersAction[argparse.ArgumentParser]")
|
||||||
|
|
||||||
|
|
||||||
|
class Handler:
|
||||||
|
"""
|
||||||
|
base handler class for command callbacks
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN(bool): (class attribute) allow running with multiple architectures
|
||||||
|
arguments(list[Callable[[SubParserAction], argparse.ArgumentParser]]): (class attribute) argument parser
|
||||||
|
methods, which will be called to create command line parsers
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Wrapper for all command line actions, though each derived class implements :func:`run()` method, it usually
|
||||||
|
must not be called directly. The recommended way is to call :func:`execute()` class method, e.g.::
|
||||||
|
|
||||||
|
>>> from ahriman.application.handlers.add import Add
|
||||||
|
>>>
|
||||||
|
>>> Add.execute(args)
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN: ClassVar[bool] = True
|
||||||
|
arguments: ClassVar[list[Callable[[SubParserAction], argparse.ArgumentParser]]]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def call(cls, args: argparse.Namespace, repository_id: RepositoryId) -> bool:
|
||||||
|
"""
|
||||||
|
additional function to wrap all calls for multiprocessing library
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: ``True`` on success, ``False`` otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
configuration = Configuration.from_path(args.configuration, repository_id)
|
||||||
|
|
||||||
|
log_handler = LogLoader.handler(args.log_handler)
|
||||||
|
LogLoader.load(repository_id, configuration, log_handler, quiet=args.quiet, report=args.report)
|
||||||
|
|
||||||
|
with Lock(args, repository_id, configuration):
|
||||||
|
cls.run(args, repository_id, configuration, report=args.report)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except ExitCode:
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
# we are basically always want to print error to stderr instead of default logger
|
||||||
|
logging.getLogger("stderr").exception("process exception")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, args: argparse.Namespace) -> int:
|
||||||
|
"""
|
||||||
|
execute function for all aru
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: 0 on success, 1 otherwise
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
MultipleArchitecturesError: if more than one architecture supplied and no multi architecture supported
|
||||||
|
"""
|
||||||
|
repositories = cls.repositories_extract(args)
|
||||||
|
|
||||||
|
# actually we do not have to spawn another process if it is single-process application, do we?
|
||||||
|
if len(repositories) > 1:
|
||||||
|
if not cls.ALLOW_MULTI_ARCHITECTURE_RUN:
|
||||||
|
raise MultipleArchitecturesError(args.command, repositories)
|
||||||
|
|
||||||
|
with Pool(len(repositories)) as pool:
|
||||||
|
result = pool.starmap(cls.call, [(args, repository_id) for repository_id in repositories])
|
||||||
|
else:
|
||||||
|
result = [cls.call(args, repositories.pop())]
|
||||||
|
|
||||||
|
return 0 if all(result) else 1
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: not implemented method
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_status(enabled: bool, status: ExplicitBool | Callable[[], ExplicitBool]) -> None:
|
||||||
|
"""
|
||||||
|
check condition and flag and raise ExitCode exception in case if it is enabled and condition match
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enabled(bool): if ``False`` no check will be performed
|
||||||
|
status(ExplicitBool | Callable[[], ExplicitBool]): return status or function to check.
|
||||||
|
``True`` means success and vice versa
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ExitCode: if result is empty and check is enabled
|
||||||
|
"""
|
||||||
|
if not enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
status = status() if callable(status) else status
|
||||||
|
if not status:
|
||||||
|
raise ExitCode
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def repositories_extract(args: argparse.Namespace) -> list[RepositoryId]:
|
||||||
|
"""
|
||||||
|
get known architectures
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[RepositoryId]: list of repository names and architectures for which tree is created
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
MissingArchitectureError: if no architecture set and automatic detection is not allowed or failed
|
||||||
|
"""
|
||||||
|
# preparse systemd repository-id argument
|
||||||
|
# we are using unescaped values, so / is not allowed here, because it is impossible to separate if from dashes
|
||||||
|
if args.repository_id is not None:
|
||||||
|
separator = "/" if "/" in args.repository_id else "-" # systemd and non-systemd identifiers
|
||||||
|
# repository parts is optional for backward compatibility
|
||||||
|
architecture, *repository_parts = args.repository_id.split(separator) # maxsplit isn't used intentionally
|
||||||
|
args.architecture = architecture
|
||||||
|
if repository_parts:
|
||||||
|
args.repository = "-".join(repository_parts) # replace slash with dash
|
||||||
|
|
||||||
|
configuration = Configuration()
|
||||||
|
configuration.load(args.configuration)
|
||||||
|
repositories = Explorer.repositories_extract(configuration, args.repository, args.architecture)
|
||||||
|
|
||||||
|
if not repositories:
|
||||||
|
raise MissingArchitectureError(args.command)
|
||||||
|
return sorted(repositories)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Help(Handler):
|
||||||
|
"""
|
||||||
|
help handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
parser: argparse.ArgumentParser = args.parser()
|
||||||
|
if args.subcommand is None:
|
||||||
|
parser.parse_args(["--help"])
|
||||||
|
else:
|
||||||
|
parser.parse_args([args.subcommand, "--help"])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for listing help subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("help", help="show help message",
|
||||||
|
description="show help message for application or command and exit")
|
||||||
|
parser.add_argument("subcommand", help="show help message for specific command", nargs="?")
|
||||||
|
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_help_parser]
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Hold(Handler):
|
||||||
|
"""
|
||||||
|
package hold handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
|
|
||||||
|
match args.action:
|
||||||
|
case Action.Remove:
|
||||||
|
for package in args.package:
|
||||||
|
client.package_hold_update(package, enabled=False)
|
||||||
|
case Action.Update:
|
||||||
|
for package in args.package:
|
||||||
|
client.package_hold_update(package, enabled=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_hold_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for hold package subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-hold", help="hold package",
|
||||||
|
description="hold package from automatic updates")
|
||||||
|
parser.add_argument("package", help="package base", nargs="+")
|
||||||
|
parser.set_defaults(action=Action.Update, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_unhold_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for unhold package subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-unhold", help="unhold package",
|
||||||
|
description="remove package hold, allowing automatic updates")
|
||||||
|
parser.add_argument("package", help="package base", nargs="+")
|
||||||
|
parser.set_defaults(action=Action.Remove, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [
|
||||||
|
_set_package_hold_parser,
|
||||||
|
_set_package_unhold_parser,
|
||||||
|
]
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class KeyImport(Handler):
|
||||||
|
"""
|
||||||
|
key import packages handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=report)
|
||||||
|
application.repository.sign.key_import(args.key_server, args.key)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_service_key_import_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for key import subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("service-key-import", aliases=["key-import"], help="import PGP key",
|
||||||
|
description="import PGP key from public sources to the repository user",
|
||||||
|
epilog="By default ahriman runs build process with package sources validation "
|
||||||
|
"(in case if signature and keys are available in PKGBUILD). This process will "
|
||||||
|
"fail in case if key is not known for build user. This subcommand can be used "
|
||||||
|
"in order to import the PGP key to user keychain.")
|
||||||
|
parser.add_argument("--key-server", help="key server for key import", default="keyserver.ubuntu.com")
|
||||||
|
parser.add_argument("key", help="PGP key to import from public server")
|
||||||
|
parser.set_defaults(architecture="", lock=None, report=False, repository="")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_service_key_import_parser]
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.build_tools.sources import Sources
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import PatchPrinter
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Patch(Handler):
|
||||||
|
"""
|
||||||
|
patch control handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=report)
|
||||||
|
application.on_start()
|
||||||
|
|
||||||
|
match args.action:
|
||||||
|
case Action.Update if args.variable is not None:
|
||||||
|
patch = Patch.patch_create_from_function(args.variable, args.patch)
|
||||||
|
Patch.patch_set_create(application, args.package, patch)
|
||||||
|
case Action.Update:
|
||||||
|
package_base, patch = Patch.patch_create_from_diff(args.package, repository_id.architecture, args.track)
|
||||||
|
Patch.patch_set_create(application, package_base, patch)
|
||||||
|
case Action.List:
|
||||||
|
Patch.patch_set_list(application, args.package, args.variable, args.exit_code)
|
||||||
|
case Action.Remove:
|
||||||
|
Patch.patch_set_remove(application, args.package, args.variable)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for new single-function patch subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("patch-add", help="add patch for PKGBUILD function",
|
||||||
|
description="create or update patched PKGBUILD function or variable",
|
||||||
|
epilog="Unlike ``patch-set-add``, this function allows to patch only one PKGBUILD "
|
||||||
|
"function, e.g. typing ``ahriman patch-add ahriman pkgver`` it will change the "
|
||||||
|
"``pkgver`` inside PKGBUILD, typing ``ahriman patch-add ahriman build()`` "
|
||||||
|
"it will change ``build()`` function inside PKGBUILD.")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.add_argument("variable", help="PKGBUILD variable or function name. If variable is a function, "
|
||||||
|
"it must end with ()")
|
||||||
|
parser.add_argument("patch", help="path to file which contains function or variable value. If not set, "
|
||||||
|
"the value will be read from stdin", type=Path, nargs="?")
|
||||||
|
parser.set_defaults(action=Action.Update, architecture="", exit_code=False, lock=None, report=False,
|
||||||
|
repository="")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for list patches subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("patch-list", help="list patch sets",
|
||||||
|
description="list available patches for the package")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("-v", "--variable", help="if set, show only patches for specified PKGBUILD variables",
|
||||||
|
action="append")
|
||||||
|
parser.set_defaults(action=Action.List, architecture="", lock=None, report=False, repository="", unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_patch_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for remove patches subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("patch-remove", help="remove patch set", description="remove patches for the package")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.add_argument("-v", "--variable",
|
||||||
|
help="should be used for single-function patches in case if you wold like "
|
||||||
|
"to remove only specified PKGBUILD variables. In case if not set, "
|
||||||
|
"it will remove all patches related to the package",
|
||||||
|
action="append")
|
||||||
|
parser.set_defaults(action=Action.Remove, architecture="", exit_code=False, lock=None, report=False,
|
||||||
|
repository="")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_patch_set_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for new full-diff patch subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("patch-set-add", help="add patch set", description="create or update source patches",
|
||||||
|
epilog="In order to add a patch set for the package you will need to:\n\n"
|
||||||
|
"1. Clone the AUR package manually.\n"
|
||||||
|
"2. Add required changes (e.g. external patches, edit PKGBUILD).\n"
|
||||||
|
"3. Run command, e.g. ``ahriman patch-set-add path/to/directory``.\n\n"
|
||||||
|
"By default it tracks ``*.patch`` and ``*.diff`` files, but this behavior "
|
||||||
|
"can be changed by using ``--track`` option.")
|
||||||
|
parser.add_argument("package", help="path to directory with changed files for patch addition/update", type=Path)
|
||||||
|
parser.add_argument("-t", "--track", help="files which has to be tracked", action="append",
|
||||||
|
default=["*.diff", "*.patch"])
|
||||||
|
parser.set_defaults(action=Action.Update, architecture="", exit_code=False, lock=None, report=False,
|
||||||
|
repository="", variable=None)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def patch_create_from_diff(sources_dir: Path, architecture: str, track: list[str]) -> tuple[str, PkgbuildPatch]:
|
||||||
|
"""
|
||||||
|
create PKGBUILD plain diff patches from sources directory
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sources_dir(Path): path to directory with the package sources
|
||||||
|
architecture(str): repository architecture
|
||||||
|
track(list[str]): track files which match the glob before creating the patch
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[str, PkgbuildPatch]: package base and created PKGBUILD patch based on the diff from master HEAD
|
||||||
|
to current files
|
||||||
|
"""
|
||||||
|
package = Package.from_build(sources_dir, architecture, None)
|
||||||
|
patch = Sources.patch_create(sources_dir, *track)
|
||||||
|
return package.base, PkgbuildPatch(None, patch)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def patch_create_from_function(variable: str, patch_path: Path | None) -> PkgbuildPatch:
|
||||||
|
"""
|
||||||
|
create single-function patch set for the package base
|
||||||
|
|
||||||
|
Args:
|
||||||
|
variable(str): function or variable name inside PKGBUILD
|
||||||
|
patch_path(Path | None): optional path to patch content. If not set, it will be read from stdin
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PkgbuildPatch: created patch for the PKGBUILD function
|
||||||
|
"""
|
||||||
|
if patch_path is None:
|
||||||
|
# pylint: disable=bad-builtin
|
||||||
|
print("Post new function or variable value below. Press Ctrl-D to finish:", file=sys.stderr)
|
||||||
|
patch = "".join(list(sys.stdin))
|
||||||
|
else:
|
||||||
|
patch = patch_path.read_text(encoding="utf8")
|
||||||
|
# remove spaces around the patch and parse to correct type
|
||||||
|
return PkgbuildPatch.parse(variable, patch.strip())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def patch_set_create(application: Application, package_base: str, patch: PkgbuildPatch) -> None:
|
||||||
|
"""
|
||||||
|
create patch set for the package base
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
package_base(str): package base
|
||||||
|
patch(PkgbuildPatch): patch descriptor
|
||||||
|
"""
|
||||||
|
application.reporter.package_patches_update(package_base, patch)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def patch_set_list(application: Application, package_base: str, variables: list[str] | None,
|
||||||
|
exit_code: bool) -> None:
|
||||||
|
"""
|
||||||
|
list patches available for the package base
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
package_base(str): package base
|
||||||
|
variables(list[str] | None): extract patches only for specified PKGBUILD variables
|
||||||
|
exit_code(bool): exit with error on empty search result
|
||||||
|
"""
|
||||||
|
patches = [
|
||||||
|
patch
|
||||||
|
for patch in application.reporter.package_patches_get(package_base, None)
|
||||||
|
if variables is None or patch.key in variables
|
||||||
|
]
|
||||||
|
Patch.check_status(exit_code, patches)
|
||||||
|
|
||||||
|
PatchPrinter(package_base, patches)(verbose=True, separator=" = ")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def patch_set_remove(application: Application, package_base: str, variables: list[str] | None) -> None:
|
||||||
|
"""
|
||||||
|
remove patch set for the package base
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
package_base(str): package base
|
||||||
|
variables(list[str] | None): remove patches only for specified PKGBUILD variables
|
||||||
|
"""
|
||||||
|
if variables is not None:
|
||||||
|
for variable in variables: # iterate over single variable
|
||||||
|
application.reporter.package_patches_remove(package_base, variable)
|
||||||
|
else:
|
||||||
|
application.reporter.package_patches_remove(package_base, None) # just pass as is
|
||||||
|
|
||||||
|
arguments = [
|
||||||
|
_set_patch_add_parser,
|
||||||
|
_set_patch_list_parser,
|
||||||
|
_set_patch_remove_parser,
|
||||||
|
_set_patch_set_add_parser,
|
||||||
|
]
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from dataclasses import replace
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import PkgbuildPrinter
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Pkgbuild(Handler):
|
||||||
|
"""
|
||||||
|
package pkgbuild handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
|
|
||||||
|
match args.action:
|
||||||
|
case Action.List:
|
||||||
|
changes = client.package_changes_get(args.package)
|
||||||
|
PkgbuildPrinter(changes)(verbose=True, separator="")
|
||||||
|
Pkgbuild.check_status(args.exit_code, changes.pkgbuild is not None)
|
||||||
|
case Action.Remove:
|
||||||
|
changes = client.package_changes_get(args.package)
|
||||||
|
client.package_changes_update(args.package, replace(changes, pkgbuild=None))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_pkgbuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package pkgbuild subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-pkgbuild", help="get package pkgbuild",
|
||||||
|
description="retrieve package PKGBUILD stored in database",
|
||||||
|
epilog="This command requests package status from the web interface "
|
||||||
|
"if it is available.")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_pkgbuild_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package pkgbuild remove subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-pkgbuild-remove", help="remove package pkgbuild",
|
||||||
|
description="remove the package PKGBUILD stored remotely")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.set_defaults(action=Action.Remove, exit_code=False, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_package_pkgbuild_parser, _set_package_pkgbuild_remove_parser]
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.utils import enum_values, extract_user
|
||||||
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.packagers import Packagers
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Rebuild(Handler):
|
||||||
|
"""
|
||||||
|
make world handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=report)
|
||||||
|
application.on_start()
|
||||||
|
|
||||||
|
packages = Rebuild.extract_packages(application, args.status, from_database=args.from_database)
|
||||||
|
packages = application.repository.packages_depend_on(packages, args.depends_on)
|
||||||
|
|
||||||
|
Rebuild.check_status(args.exit_code, packages)
|
||||||
|
if args.dry_run:
|
||||||
|
application.print_updates(packages, log_fn=print)
|
||||||
|
return
|
||||||
|
|
||||||
|
result = application.update(packages, Packagers(args.username), bump_pkgrel=args.increment)
|
||||||
|
Rebuild.check_status(args.exit_code, not result.is_empty)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for repository rebuild subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("repo-rebuild", aliases=["rebuild"], help="rebuild repository",
|
||||||
|
description="force rebuild whole repository")
|
||||||
|
parser.add_argument("--depends-on", help="only rebuild packages that depend on specified packages",
|
||||||
|
action="append")
|
||||||
|
parser.add_argument("--dry-run", help="just perform check for packages without rebuild process itself",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--from-database",
|
||||||
|
help="read packages from database instead of filesystem. This feature in particular is "
|
||||||
|
"required in case if you would like to restore repository from another repository "
|
||||||
|
"instance. Note, however, that in order to restore packages you need to have original "
|
||||||
|
"ahriman instance run with web service and have run repo-update at least once.",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--increment", help="increment package release (pkgrel) on duplicate",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("-s", "--status", help="filter packages by status. Requires --from-database to be set",
|
||||||
|
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum))
|
||||||
|
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extract_packages(application: Application, status: BuildStatusEnum | None, *,
|
||||||
|
from_database: bool) -> list[Package]:
|
||||||
|
"""
|
||||||
|
extract packages from database file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
status(BuildStatusEnum | None): optional filter by package status
|
||||||
|
from_database(bool): extract packages from database instead of repository filesystem
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Package]: list of packages which were stored in database
|
||||||
|
"""
|
||||||
|
if from_database:
|
||||||
|
return [
|
||||||
|
package
|
||||||
|
for (package, last_status) in application.reporter.package_get(None)
|
||||||
|
if status is None or last_status.status == status
|
||||||
|
]
|
||||||
|
|
||||||
|
return application.repository.packages()
|
||||||
|
|
||||||
|
arguments = [_set_repo_rebuild_parser]
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Reload(Handler):
|
||||||
|
"""
|
||||||
|
web server reload handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
|
client.configuration_reload()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_web_reload_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for web reload subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("web-reload", help="reload configuration",
|
||||||
|
description="reload web server configuration",
|
||||||
|
epilog="This method forces the web server to reload its configuration. "
|
||||||
|
"Note, however, that this method does not apply all configuration changes "
|
||||||
|
"(like ports, authentication, etc)")
|
||||||
|
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_web_reload_parser]
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Remove(Handler):
|
||||||
|
"""
|
||||||
|
remove packages handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=report)
|
||||||
|
application.on_start()
|
||||||
|
application.remove(args.package)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package removal subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-remove", aliases=["remove"], help="remove package",
|
||||||
|
description="remove package from the repository")
|
||||||
|
parser.add_argument("package", help="package name or base", nargs="+")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_package_remove_parser]
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import StringPrinter
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveUnknown(Handler):
|
||||||
|
"""
|
||||||
|
remove unknown packages handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=report)
|
||||||
|
application.on_start()
|
||||||
|
unknown_packages = application.unknown()
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
for package in sorted(unknown_packages):
|
||||||
|
StringPrinter(package)(verbose=False)
|
||||||
|
return
|
||||||
|
|
||||||
|
application.remove(unknown_packages)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_repo_remove_unknown_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for remove unknown packages subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("repo-remove-unknown", aliases=["remove-unknown"], help="remove unknown packages",
|
||||||
|
description="remove packages which are missing in AUR and do not have local PKGBUILDs")
|
||||||
|
parser.add_argument("--dry-run", help="just perform check for packages without removal", action="store_true")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_repo_remove_unknown_parser]
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import RepositoryPrinter
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Repositories(Handler):
|
||||||
|
"""
|
||||||
|
repositories listing handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
dummy_args = argparse.Namespace(
|
||||||
|
architecture=None,
|
||||||
|
configuration=args.configuration,
|
||||||
|
repository=None,
|
||||||
|
repository_id=None,
|
||||||
|
)
|
||||||
|
for repository in cls.repositories_extract(dummy_args):
|
||||||
|
RepositoryPrinter(repository)(verbose=not args.id_only)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_service_repositories(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for repositories listing
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("service-repositories", help="show repositories",
|
||||||
|
description="list all available repositories")
|
||||||
|
parser.add_argument("--id-only", help="show machine readable identifier instead",
|
||||||
|
action=argparse.BooleanOptionalAction, default=False)
|
||||||
|
parser.set_defaults(architecture="", lock=None, report=False, repository="", unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_service_repositories]
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
import tarfile
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Restore(Handler):
|
||||||
|
"""
|
||||||
|
restore packages handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
with tarfile.open(args.path) as archive:
|
||||||
|
archive.extractall(path=args.output, filter="data")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for repository restore subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("repo-restore", help="restore repository data",
|
||||||
|
description="restore settings and database")
|
||||||
|
parser.add_argument("path", help="path of the input archive", type=Path)
|
||||||
|
parser.add_argument("-o", "--output", help="root path of the extracted files", type=Path, default=Path("/"))
|
||||||
|
parser.set_defaults(architecture="", lock=None, report=False, repository="", unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_repo_restore_parser]
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from dataclasses import replace
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.add import Add
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.exceptions import UnknownPackageError
|
||||||
|
from ahriman.core.utils import extract_user
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.package_source import PackageSource
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Rollback(Handler):
|
||||||
|
"""
|
||||||
|
package rollback handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=report)
|
||||||
|
application.on_start()
|
||||||
|
|
||||||
|
package = Rollback.package_load(application, args.package, args.version)
|
||||||
|
artifacts = Rollback.package_artifacts(application, package)
|
||||||
|
|
||||||
|
args.package = [str(artifact) for artifact in artifacts]
|
||||||
|
Add.perform_action(application, args)
|
||||||
|
|
||||||
|
if args.hold:
|
||||||
|
application.reporter.package_hold_update(package.base, enabled=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_rollback_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package rollback subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-rollback", help="rollback package",
|
||||||
|
description="rollback package to specified version from archives")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.add_argument("version", help="package version")
|
||||||
|
parser.add_argument("--hold", help="hold package afterwards",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
|
||||||
|
parser.set_defaults(aur=False, changes=False, check_files=False, dependencies=False, dry_run=False,
|
||||||
|
exit_code=True, increment=False, now=True, local=False, manual=False, refresh=False,
|
||||||
|
source=PackageSource.Archive, variable=None, vcs=False)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def package_artifacts(application: Application, package: Package) -> list[Path]:
|
||||||
|
"""
|
||||||
|
look for requested package artifacts and return paths to them
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
package(Package): package descriptor
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Path]: paths to found artifacts
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UnknownPackageError: if artifacts do not exist
|
||||||
|
"""
|
||||||
|
# lookup for built artifacts
|
||||||
|
artifacts = application.repository.package_archives_lookup(package)
|
||||||
|
if not artifacts:
|
||||||
|
raise UnknownPackageError(package.base)
|
||||||
|
return artifacts
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def package_load(application: Application, package_base: str, version: str) -> Package:
|
||||||
|
"""
|
||||||
|
load package from repository, while setting requested version
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
package_base(str): package base
|
||||||
|
version(str): package version
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Package: loaded package
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UnknownPackageError: if package does not exist
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
package, _ = next(iter(application.reporter.package_get(package_base)))
|
||||||
|
return replace(package, version=version)
|
||||||
|
except StopIteration:
|
||||||
|
raise UnknownPackageError(package_base) from None
|
||||||
|
|
||||||
|
arguments = [_set_package_rollback_parser]
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Run(Handler):
|
||||||
|
"""
|
||||||
|
multicommand handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
parser = args.parser()
|
||||||
|
for command in args.command:
|
||||||
|
status = Run.run_command(shlex.split(command), parser)
|
||||||
|
Run.check_status(True, status)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_service_run(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for multicommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("service-run", aliases=["run"], help="run multiple commands",
|
||||||
|
description="run multiple commands on success run of the previous command",
|
||||||
|
epilog="Commands must be quoted by using usual bash rules. Processes will be spawned "
|
||||||
|
"under the same user as this command.")
|
||||||
|
parser.add_argument("command", help="command to be run (quoted) without ``ahriman``", nargs="+")
|
||||||
|
parser.set_defaults(architecture="", lock=None, report=False, repository="", unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def run_command(command: list[str], parser: argparse.ArgumentParser) -> bool:
|
||||||
|
"""
|
||||||
|
run command specified by the argument
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command(list[str]): command to run
|
||||||
|
parser(argparse.ArgumentParser): generated argument parser
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: status of the command
|
||||||
|
"""
|
||||||
|
args = parser.parse_args(command)
|
||||||
|
handler: Handler = args.handler
|
||||||
|
return handler.execute(args) == 0
|
||||||
|
|
||||||
|
arguments = [_set_service_run]
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from collections.abc import Callable, Iterable
|
||||||
|
from dataclasses import fields
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.alpm.remote import AUR, Official
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.exceptions import OptionError
|
||||||
|
from ahriman.core.formatters import AurPrinter
|
||||||
|
from ahriman.core.types import Comparable
|
||||||
|
from ahriman.models.aur_package import AURPackage
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Search(Handler):
|
||||||
|
"""
|
||||||
|
packages search handler
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
SORT_FIELDS(set[str]): (class attribute) allowed fields to sort the package list
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||||
|
SORT_FIELDS: ClassVar[set[str]] = {
|
||||||
|
field.name
|
||||||
|
for field in fields(AURPackage)
|
||||||
|
if field.default_factory is not list
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
official_packages_list = Official.multisearch(*args.search)
|
||||||
|
aur_packages_list = AUR.multisearch(*args.search)
|
||||||
|
non_empty = bool(official_packages_list or aur_packages_list)
|
||||||
|
Search.check_status(args.exit_code, non_empty)
|
||||||
|
|
||||||
|
for packages_list in (official_packages_list, aur_packages_list):
|
||||||
|
# keep sorting by packages source
|
||||||
|
for package in Search.sort(packages_list, args.sort_by):
|
||||||
|
AurPrinter(package)(verbose=args.info)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for AUR search subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("aur-search", aliases=["search"], help="search for package",
|
||||||
|
description="search for package in AUR using API")
|
||||||
|
parser.add_argument("search",
|
||||||
|
help="search terms, can be specified multiple times, the result will match all terms",
|
||||||
|
nargs="+")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--info", help="show additional package information",
|
||||||
|
action=argparse.BooleanOptionalAction, default=False)
|
||||||
|
parser.add_argument("--sort-by",
|
||||||
|
help="sort field by this field. In case if two packages have the same value of "
|
||||||
|
"the specified field, they will be always sorted by name",
|
||||||
|
default="name", choices=sorted(Search.SORT_FIELDS))
|
||||||
|
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sort(packages: Iterable[AURPackage], sort_by: str) -> list[AURPackage]:
|
||||||
|
"""
|
||||||
|
sort package list by specified field
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packages(Iterable[AURPackage]): packages list to sort
|
||||||
|
sort_by(str): AUR package field name to sort by
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[AURPackage]: sorted list for packages
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
OptionError: if search fields is not in list of allowed ones
|
||||||
|
"""
|
||||||
|
if sort_by not in Search.SORT_FIELDS:
|
||||||
|
raise OptionError(sort_by)
|
||||||
|
# always sort by package name at the last
|
||||||
|
# well technically it is not a string, but we can deal with it
|
||||||
|
comparator: Callable[[AURPackage], Comparable] = \
|
||||||
|
lambda package: (getattr(package, sort_by), package.name)
|
||||||
|
return sorted(packages, key=comparator)
|
||||||
|
|
||||||
|
arguments = [_set_aur_search_parser]
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman import __version__
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import UpdatePrinter
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceUpdates(Handler):
|
||||||
|
"""
|
||||||
|
service updates handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
remote = Package.from_aur("ahriman", None)
|
||||||
|
_, release = remote.version.rsplit("-", maxsplit=1) # we don't store pkgrel locally, so we just append it
|
||||||
|
local_version = f"{__version__}-{release}"
|
||||||
|
|
||||||
|
# technically we would like to compare versions, but it is fine to raise an exception in case if locally
|
||||||
|
# installed package is newer than in AUR
|
||||||
|
same_version = remote.version == local_version
|
||||||
|
if same_version:
|
||||||
|
return
|
||||||
|
|
||||||
|
UpdatePrinter(remote, local_version)(verbose=True, separator=" -> ")
|
||||||
|
ServiceUpdates.check_status(args.exit_code, same_version)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_help_updates_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for service update check subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("help-updates", help="check for service updates",
|
||||||
|
description="request AUR for current version and compare with current service version")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit code if updates available",
|
||||||
|
action="store_true")
|
||||||
|
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_help_updates_parser]
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from pwd import getpwuid
|
||||||
|
from typing import ClassVar
|
||||||
|
from urllib.parse import quote_plus as url_encode
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.exceptions import MissingArchitectureError
|
||||||
|
from ahriman.core.utils import enum_values
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
from ahriman.models.repository_paths import RepositoryPaths
|
||||||
|
from ahriman.models.sign_settings import SignSettings
|
||||||
|
from ahriman.models.user import User
|
||||||
|
|
||||||
|
|
||||||
|
class Setup(Handler):
|
||||||
|
"""
|
||||||
|
setup handler
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
ARCHBUILD_COMMAND_PATH(Path): (class attribute) default devtools command
|
||||||
|
MIRRORLIST_PATH(Path): (class attribute) path to pacman default mirrorlist (used by multilib repository)
|
||||||
|
SUDOERS_DIR_PATH(Path): (class attribute) path to sudoers.d includes directory
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
ARCHBUILD_COMMAND_PATH: ClassVar[Path] = Path("/") / "usr" / "bin" / "archbuild"
|
||||||
|
MIRRORLIST_PATH: ClassVar[Path] = Path("/") / "etc" / "pacman.d" / "mirrorlist"
|
||||||
|
SUDOERS_DIR_PATH: ClassVar[Path] = Path("/") / "etc" / "sudoers.d"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
# special check for args to avoid auto definition for setup command
|
||||||
|
if args.architecture is None or args.repository is None:
|
||||||
|
raise MissingArchitectureError(args.command)
|
||||||
|
|
||||||
|
Setup.configuration_create_ahriman(args, repository_id, configuration)
|
||||||
|
configuration.reload()
|
||||||
|
|
||||||
|
application = Application(repository_id, configuration, report=report)
|
||||||
|
|
||||||
|
# basically we create configuration here as root, but it is ok, because those files are only used for reading
|
||||||
|
Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, application.repository.paths)
|
||||||
|
Setup.executable_create(application.repository.paths, repository_id)
|
||||||
|
repository_server = f"file://{application.repository.paths.repository}" if args.server is None else args.server
|
||||||
|
Setup.configuration_create_devtools(
|
||||||
|
repository_id, args.from_configuration, args.mirror, args.multilib, repository_server)
|
||||||
|
Setup.configuration_create_sudo(application.repository.paths, repository_id)
|
||||||
|
|
||||||
|
# finish initialization
|
||||||
|
with application.repository.paths.preserve_owner():
|
||||||
|
application.repository.repo.init()
|
||||||
|
# lazy database sync
|
||||||
|
application.repository.pacman.handle # pylint: disable=pointless-statement
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_service_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for setup subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("service-setup", aliases=["init", "repo-init", "repo-setup", "setup"],
|
||||||
|
help="initial service configuration",
|
||||||
|
description="create initial service configuration, requires root",
|
||||||
|
epilog="Create minimal configuration for the service according to provided options.")
|
||||||
|
parser.add_argument("--build-as-user", help="force makepkg user to the specific one")
|
||||||
|
parser.add_argument("--from-configuration", help="path to default devtools pacman configuration",
|
||||||
|
type=Path,
|
||||||
|
default=Path("/") / "usr" / "share" / "devtools" / "pacman.conf.d" / "extra.conf")
|
||||||
|
parser.add_argument("--generate-salt", help="generate salt for user passwords",
|
||||||
|
action=argparse.BooleanOptionalAction, default=False)
|
||||||
|
parser.add_argument("--makeflags-jobs",
|
||||||
|
help="append MAKEFLAGS variable with parallelism set to number of cores",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--mirror", help="use the specified explicitly mirror instead of including mirrorlist")
|
||||||
|
parser.add_argument("--multilib", help="add or do not multilib repository",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--packager", help="packager name and email", required=True)
|
||||||
|
parser.add_argument("--server", help="server to be used for devtools. If none set, local files will be used")
|
||||||
|
parser.add_argument("--sign-key", help="sign key id")
|
||||||
|
parser.add_argument("--sign-target", help="sign options", action="append",
|
||||||
|
type=SignSettings.from_option, choices=enum_values(SignSettings))
|
||||||
|
parser.add_argument("--web-port", help="port of the web service", type=int)
|
||||||
|
parser.add_argument("--web-unix-socket", help="path to unix socket used for interprocess communications",
|
||||||
|
type=Path)
|
||||||
|
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def build_command(root: Path, repository_id: RepositoryId) -> Path:
|
||||||
|
"""
|
||||||
|
generate build command name
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(Path): root directory for the build command (must be root of the repository)
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path: valid devtools command name
|
||||||
|
"""
|
||||||
|
return root / f"{repository_id.name}-{repository_id.architecture}-build"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def configuration_create_ahriman(args: argparse.Namespace, repository_id: RepositoryId,
|
||||||
|
root: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
create service specific configuration
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
root(Configuration): root configuration instance
|
||||||
|
"""
|
||||||
|
configuration = Configuration()
|
||||||
|
|
||||||
|
section = Configuration.section_name("build", repository_id.name, repository_id.architecture)
|
||||||
|
build_command = Setup.build_command(root.repository_paths.root, repository_id)
|
||||||
|
configuration.set_option(section, "build_command", str(build_command))
|
||||||
|
configuration.set_option("repository", "name", repository_id.name) # backward compatibility for docker
|
||||||
|
if args.build_as_user is not None:
|
||||||
|
configuration.set_option(section, "makechrootpkg_flags", f"-U {args.build_as_user}")
|
||||||
|
|
||||||
|
section = Configuration.section_name("alpm", repository_id.name, repository_id.architecture)
|
||||||
|
if args.mirror is not None:
|
||||||
|
configuration.set_option(section, "mirror", args.mirror)
|
||||||
|
if not args.multilib:
|
||||||
|
repositories = filter(lambda r: r != "multilib", root.getlist("alpm", "repositories"))
|
||||||
|
configuration.set_option(section, "repositories", " ".join(repositories))
|
||||||
|
|
||||||
|
section = Configuration.section_name("sign", repository_id.name, repository_id.architecture)
|
||||||
|
if args.sign_key is not None:
|
||||||
|
sign_targets = args.sign_target or []
|
||||||
|
configuration.set_option(section, "target", " ".join([target.name.lower() for target in sign_targets]))
|
||||||
|
configuration.set_option(section, "key", args.sign_key)
|
||||||
|
|
||||||
|
if args.web_port is not None:
|
||||||
|
configuration.set_option("web", "port", str(args.web_port))
|
||||||
|
if (host := root.get("web", "host", fallback=None)) is not None:
|
||||||
|
configuration.set_option("status", "address", f"http://{host}:{args.web_port}")
|
||||||
|
if args.web_unix_socket is not None:
|
||||||
|
unix_socket = str(args.web_unix_socket)
|
||||||
|
configuration.set_option("web", "unix_socket", unix_socket)
|
||||||
|
configuration.set_option("status", "address", f"http+unix://{url_encode(unix_socket)}")
|
||||||
|
|
||||||
|
if args.generate_salt:
|
||||||
|
configuration.set_option("auth", "salt", User.generate_password(20))
|
||||||
|
|
||||||
|
(root.include / "00-setup-overrides.ini").unlink(missing_ok=True) # remove old-style configuration
|
||||||
|
target = root.include / f"00-setup-overrides-{repository_id.id}.ini"
|
||||||
|
with target.open("w", encoding="utf8") as ahriman_configuration:
|
||||||
|
configuration.write(ahriman_configuration)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def configuration_create_devtools(repository_id: RepositoryId, source: Path, mirror: str | None,
|
||||||
|
multilib: bool, repository_server: str) -> None:
|
||||||
|
"""
|
||||||
|
create configuration for devtools based on ``source`` configuration
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
devtools does not allow to specify the pacman configuration, thus we still have to use configuration in /usr
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
source(Path): path to source configuration file
|
||||||
|
mirror(str | None): link to package server mirror
|
||||||
|
multilib(bool): add or do not multilib repository to the configuration
|
||||||
|
repository_server(str): url of the repository
|
||||||
|
"""
|
||||||
|
# allow_no_value=True is required because pacman uses boolean configuration in which just keys present
|
||||||
|
# (e.g. NoProgressBar) which will lead to exception. allow_multi_key=False is set just for fun
|
||||||
|
configuration = Configuration(allow_no_value=True, allow_multi_key=False)
|
||||||
|
# preserve case
|
||||||
|
# stupid mypy thinks that it is impossible
|
||||||
|
configuration.optionxform = lambda optionstr: optionstr # type: ignore[method-assign]
|
||||||
|
|
||||||
|
# load default configuration first
|
||||||
|
# we cannot use Include here because it will be copied to new chroot, thus no includes there
|
||||||
|
configuration.read(source)
|
||||||
|
|
||||||
|
# set our architecture now
|
||||||
|
configuration.set_option("options", "Architecture", repository_id.architecture)
|
||||||
|
|
||||||
|
# add multilib
|
||||||
|
if multilib:
|
||||||
|
configuration.set_option("multilib", "Include", str(Setup.MIRRORLIST_PATH))
|
||||||
|
|
||||||
|
# override Include option to Server in case if mirror option set
|
||||||
|
if mirror is not None:
|
||||||
|
for section in filter(lambda s: s != "options", configuration.sections()):
|
||||||
|
if configuration.get(section, "Include", fallback=None) != str(Setup.MIRRORLIST_PATH):
|
||||||
|
continue
|
||||||
|
configuration.remove_option(section, "Include")
|
||||||
|
configuration.set_option(section, "Server", mirror)
|
||||||
|
|
||||||
|
# add repository itself
|
||||||
|
configuration.set_option(repository_id.name, "SigLevel", "Never") # we don't care
|
||||||
|
configuration.set_option(repository_id.name, "Server", repository_server)
|
||||||
|
|
||||||
|
target = source.parent / f"{repository_id.name}-{repository_id.architecture}.conf"
|
||||||
|
with target.open("w", encoding="utf8") as devtools_configuration:
|
||||||
|
configuration.write(devtools_configuration)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def configuration_create_makepkg(packager: str, makeflags_jobs: bool, paths: RepositoryPaths) -> None:
|
||||||
|
"""
|
||||||
|
create configuration for makepkg
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packager(str): packager identifier (e.g. name, email)
|
||||||
|
makeflags_jobs(bool): set MAKEFLAGS variable to number of cores
|
||||||
|
paths(RepositoryPaths): repository paths instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
content = f"PACKAGER='{packager}'\n"
|
||||||
|
if makeflags_jobs:
|
||||||
|
content += "MAKEFLAGS=\"-j$(nproc)\"\n"
|
||||||
|
|
||||||
|
uid, _ = paths.root_owner
|
||||||
|
home_dir = Path(getpwuid(uid).pw_dir)
|
||||||
|
(home_dir / ".makepkg.conf").write_text(content, encoding="utf8")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def configuration_create_sudo(paths: RepositoryPaths, repository_id: RepositoryId) -> None:
|
||||||
|
"""
|
||||||
|
create configuration to run build command with sudo without password
|
||||||
|
|
||||||
|
Args:
|
||||||
|
paths(RepositoryPaths): repository paths instance
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
"""
|
||||||
|
command = Setup.build_command(paths.root, repository_id)
|
||||||
|
sudoers_file = Setup.build_command(Setup.SUDOERS_DIR_PATH, repository_id)
|
||||||
|
sudoers_file.write_text(f"ahriman ALL=(ALL) NOPASSWD:SETENV: {command} *\n", encoding="utf8")
|
||||||
|
sudoers_file.chmod(0o400) # security!
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def executable_create(paths: RepositoryPaths, repository_id: RepositoryId) -> None:
|
||||||
|
"""
|
||||||
|
create executable for the service
|
||||||
|
|
||||||
|
Args:
|
||||||
|
paths(RepositoryPaths): repository paths instance
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
"""
|
||||||
|
command = Setup.build_command(paths.root, repository_id)
|
||||||
|
command.unlink(missing_ok=True)
|
||||||
|
command.symlink_to(Setup.ARCHBUILD_COMMAND_PATH)
|
||||||
|
|
||||||
|
arguments = [_set_service_setup_parser]
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.application.interactive_shell import InteractiveShell
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import StringPrinter
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Shell(Handler):
|
||||||
|
"""
|
||||||
|
python shell handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
if args.verbose:
|
||||||
|
# licensed by https://creativecommons.org/licenses/by-sa/3.0
|
||||||
|
path = Path(sys.prefix) / "share" / "ahriman" / "templates" / "shell"
|
||||||
|
StringPrinter(path.read_text(encoding="utf8"))(verbose=False)
|
||||||
|
|
||||||
|
local_variables = {
|
||||||
|
"architecture": repository_id.architecture,
|
||||||
|
"configuration": configuration,
|
||||||
|
"repository_id": repository_id,
|
||||||
|
}
|
||||||
|
console = InteractiveShell(locals=local_variables)
|
||||||
|
|
||||||
|
match args.code:
|
||||||
|
case None:
|
||||||
|
console.interact()
|
||||||
|
case snippet:
|
||||||
|
console.runcode(snippet)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_service_shell_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for shell subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("service-shell", aliases=["shell"], help="invoke python shell",
|
||||||
|
description="drop into python shell")
|
||||||
|
parser.add_argument("code", help="instead of dropping into shell, just execute the specified code", nargs="?")
|
||||||
|
parser.add_argument("-v", "--verbose", help=argparse.SUPPRESS, action="store_true")
|
||||||
|
parser.add_argument("-o", "--output", help="output commands and result to the file", type=Path)
|
||||||
|
parser.set_defaults(lock=None, report=False)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_service_shell_parser]
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Sign(Handler):
|
||||||
|
"""
|
||||||
|
(re-)sign handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
Application(repository_id, configuration, report=report).sign(args.package)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_repo_sign_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for sign subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("repo-sign", aliases=["sign"], help="sign packages",
|
||||||
|
description="(re-)sign packages and repository database according to current settings",
|
||||||
|
epilog="Sign repository and/or packages as configured.")
|
||||||
|
parser.add_argument("package", help="sign only specified packages", nargs="*")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_repo_sign_parser]
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import EventStatsPrinter, PackageStatsPrinter, RepositoryStatsPrinter
|
||||||
|
from ahriman.core.utils import enum_values, pretty_datetime
|
||||||
|
from ahriman.models.event import Event, EventType
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Statistics(Handler):
|
||||||
|
"""
|
||||||
|
repository statistics handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=True)
|
||||||
|
|
||||||
|
from_date = to_date = None
|
||||||
|
if (value := args.from_date) is not None:
|
||||||
|
from_date = datetime.datetime.fromisoformat(value).timestamp()
|
||||||
|
if (value := args.to_date) is not None:
|
||||||
|
to_date = datetime.datetime.fromisoformat(value).timestamp()
|
||||||
|
|
||||||
|
events = application.reporter.event_get(args.event, args.package, from_date, to_date, args.limit, args.offset)
|
||||||
|
|
||||||
|
match args.package:
|
||||||
|
case None:
|
||||||
|
RepositoryStatsPrinter(repository_id, application.reporter.statistics())(verbose=True)
|
||||||
|
Statistics.stats_per_package(args.event, events, args.chart)
|
||||||
|
case _:
|
||||||
|
Statistics.stats_for_package(args.event, events, args.chart)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_repo_statistics_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for repository statistics subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("repo-statistics", help="repository statistics",
|
||||||
|
description="fetch repository statistics")
|
||||||
|
parser.add_argument("package", help="fetch only events for the specified package", nargs="?")
|
||||||
|
parser.add_argument("--chart", help="create updates chart and save it to the specified path", type=Path)
|
||||||
|
parser.add_argument("-e", "--event", help="event type filter",
|
||||||
|
type=EventType, choices=enum_values(EventType), default=EventType.PackageUpdated)
|
||||||
|
parser.add_argument("--from-date", help="only fetch events which are newer than the date")
|
||||||
|
parser.add_argument("--limit", help="limit response by specified amount of events", type=int, default=-1)
|
||||||
|
parser.add_argument("--offset", help="skip specified amount of events", type=int, default=0)
|
||||||
|
parser.add_argument("--to-date", help="only fetch events which are older than the date")
|
||||||
|
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def event_stats(event_type: str, events: list[Event]) -> None:
|
||||||
|
"""
|
||||||
|
calculate event stats
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type(str): event type
|
||||||
|
events(list[Event]): list of events
|
||||||
|
"""
|
||||||
|
times = [event.get("took") for event in events if event.get("took") is not None]
|
||||||
|
EventStatsPrinter(f"{event_type} duration, s", times)(verbose=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def plot_packages(event_type: str, events: dict[str, int], path: Path) -> None:
|
||||||
|
"""
|
||||||
|
plot packages frequency
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type(str): event type
|
||||||
|
events(dict[str, int]): list of events
|
||||||
|
path(Path): path to save plot
|
||||||
|
"""
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
|
||||||
|
x, y = list(events.keys()), list(events.values())
|
||||||
|
plt.bar(x, y)
|
||||||
|
|
||||||
|
plt.xlabel("Package base")
|
||||||
|
plt.ylabel("Frequency")
|
||||||
|
plt.title(f"Frequency of the {event_type} event per package")
|
||||||
|
|
||||||
|
plt.savefig(path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def plot_times(event_type: str, events: list[Event], path: Path) -> None:
|
||||||
|
"""
|
||||||
|
plot events timeline
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type(str): event type
|
||||||
|
events(list[Event]): list of events
|
||||||
|
path(Path): path to save plot
|
||||||
|
"""
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
|
||||||
|
figure = plt.figure()
|
||||||
|
|
||||||
|
x, y = zip(*[(pretty_datetime(event.created), event.get("took")) for event in events])
|
||||||
|
plt.plot(x, y)
|
||||||
|
|
||||||
|
plt.xlabel("Event timestamp")
|
||||||
|
plt.ylabel("Duration, s")
|
||||||
|
plt.title(f"Duration of the {event_type} event")
|
||||||
|
figure.autofmt_xdate()
|
||||||
|
|
||||||
|
plt.savefig(path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def stats_for_package(event_type: str, events: list[Event], chart_path: Path | None) -> None:
|
||||||
|
"""
|
||||||
|
calculate statistics for a package
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type(str): event type
|
||||||
|
events(list[Event]): list of events
|
||||||
|
chart_path(Path): path to save plot if any
|
||||||
|
"""
|
||||||
|
# event statistics
|
||||||
|
Statistics.event_stats(event_type, events)
|
||||||
|
|
||||||
|
# chart if enabled
|
||||||
|
if chart_path is not None:
|
||||||
|
Statistics.plot_times(event_type, events, chart_path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def stats_per_package(event_type: str, events: list[Event], chart_path: Path | None) -> None:
|
||||||
|
"""
|
||||||
|
calculate overall statistics
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type(str): event type
|
||||||
|
events(list[Event]): list of events
|
||||||
|
chart_path(Path): path to save plot if any
|
||||||
|
"""
|
||||||
|
key: Callable[[Event], str] = lambda event: event.object_id
|
||||||
|
by_object_id = {
|
||||||
|
object_id: len(list(related))
|
||||||
|
for object_id, related in itertools.groupby(sorted(events, key=key), key=key)
|
||||||
|
}
|
||||||
|
|
||||||
|
# distribution per package
|
||||||
|
PackageStatsPrinter(by_object_id)(verbose=True)
|
||||||
|
EventStatsPrinter(f"{event_type} frequency", list(by_object_id.values()))(verbose=True)
|
||||||
|
|
||||||
|
# event statistics
|
||||||
|
Statistics.event_stats(event_type, events)
|
||||||
|
|
||||||
|
# chart if enabled
|
||||||
|
if chart_path is not None:
|
||||||
|
Statistics.plot_packages(event_type, by_object_id, chart_path)
|
||||||
|
|
||||||
|
arguments = [_set_repo_statistics_parser]
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import PackagePrinter, StatusPrinter
|
||||||
|
from ahriman.core.types import Comparable
|
||||||
|
from ahriman.core.utils import enum_values
|
||||||
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Status(Handler):
|
||||||
|
"""
|
||||||
|
package status handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
# we are using reporter here
|
||||||
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
|
if args.ahriman:
|
||||||
|
service_status = client.status_get()
|
||||||
|
StatusPrinter(service_status.status)(verbose=args.info)
|
||||||
|
if args.package:
|
||||||
|
packages: list[tuple[Package, BuildStatus]] = sum(
|
||||||
|
(client.package_get(base) for base in args.package),
|
||||||
|
start=[])
|
||||||
|
else:
|
||||||
|
packages = client.package_get(None)
|
||||||
|
|
||||||
|
Status.check_status(args.exit_code, packages)
|
||||||
|
|
||||||
|
comparator: Callable[[tuple[Package, BuildStatus]], Comparable] = lambda item: item[0].base
|
||||||
|
filter_fn: Callable[[tuple[Package, BuildStatus]], bool] = \
|
||||||
|
lambda item: args.status is None or item[1].status == args.status
|
||||||
|
for package, package_status in sorted(filter(filter_fn, packages), key=comparator):
|
||||||
|
PackagePrinter(package, package_status)(verbose=args.info)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_status_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package status subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-status", aliases=["status"], help="get package status",
|
||||||
|
description="request status of the package",
|
||||||
|
epilog="This command requests package status from the web interface "
|
||||||
|
"if it is available.")
|
||||||
|
parser.add_argument("package", help="filter status by package base", nargs="*")
|
||||||
|
parser.add_argument("--ahriman", help="get service status itself", action="store_true")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--info", help="show additional package information",
|
||||||
|
action=argparse.BooleanOptionalAction, default=False)
|
||||||
|
parser.add_argument("-s", "--status", help="filter packages by status",
|
||||||
|
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum))
|
||||||
|
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_package_status_parser]
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.utils import enum_values
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class StatusUpdate(Handler):
|
||||||
|
"""
|
||||||
|
status update handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
|
|
||||||
|
match args.action:
|
||||||
|
case Action.Update if args.package:
|
||||||
|
# update packages statuses
|
||||||
|
for package in args.package:
|
||||||
|
client.package_status_update(package, args.status)
|
||||||
|
case Action.Update:
|
||||||
|
# update service status
|
||||||
|
client.status_update(args.status)
|
||||||
|
case Action.Remove:
|
||||||
|
for package in args.package:
|
||||||
|
client.package_remove(package)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_status_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package status remove subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-status-remove", help="remove package status",
|
||||||
|
description="remove the package from the status page",
|
||||||
|
epilog="Please note that this subcommand does not remove the package itself, it just "
|
||||||
|
"clears the status page.")
|
||||||
|
parser.add_argument("package", help="remove specified packages from status page", nargs="+")
|
||||||
|
parser.set_defaults(action=Action.Remove, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package status update subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-status-update", aliases=["status-update"], help="update package status",
|
||||||
|
description="update package status on the status page")
|
||||||
|
parser.add_argument("package", help="set status for specified packages. "
|
||||||
|
"If no packages supplied, service status will be updated",
|
||||||
|
nargs="*")
|
||||||
|
parser.add_argument("-s", "--status", help="new package build status",
|
||||||
|
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum), default=BuildStatusEnum.Success)
|
||||||
|
parser.set_defaults(action=Action.Update, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_repo_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for repository status update subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("repo-status-update", help="update repository status",
|
||||||
|
description="update repository status on the status page")
|
||||||
|
parser.add_argument("-s", "--status", help="new status",
|
||||||
|
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum), default=BuildStatusEnum.Success)
|
||||||
|
parser.set_defaults(action=Action.Update, lock=None, package=[], quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [
|
||||||
|
_set_package_status_remove_parser,
|
||||||
|
_set_package_status_update_parser,
|
||||||
|
_set_repo_status_update_parser,
|
||||||
|
]
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import StringPrinter, TreePrinter
|
||||||
|
from ahriman.core.tree import Tree
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Structure(Handler):
|
||||||
|
"""
|
||||||
|
dump repository structure handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=report)
|
||||||
|
partitions = Tree.partition(application.repository.packages(), count=args.partitions)
|
||||||
|
|
||||||
|
for partition_id, partition in enumerate(partitions):
|
||||||
|
StringPrinter(f"partition #{partition_id}")(verbose=False)
|
||||||
|
|
||||||
|
tree = Tree.resolve(partition)
|
||||||
|
for num, level in enumerate(tree):
|
||||||
|
TreePrinter(num, level)(verbose=True, separator=" ")
|
||||||
|
|
||||||
|
# empty line
|
||||||
|
StringPrinter("")(verbose=False)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_repo_tree_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for repository tree subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("repo-tree", help="dump repository tree",
|
||||||
|
description="dump repository tree based on packages dependencies")
|
||||||
|
parser.add_argument("-p", "--partitions", help="also divide packages by independent partitions",
|
||||||
|
type=int, default=1)
|
||||||
|
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_repo_tree_parser]
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.utils import symlink_relative, walk
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
from ahriman.models.repository_paths import RepositoryPaths
|
||||||
|
|
||||||
|
|
||||||
|
class TreeMigrate(Handler):
|
||||||
|
"""
|
||||||
|
tree migration handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
current_tree = configuration.repository_paths
|
||||||
|
target_tree = RepositoryPaths(current_tree.root, current_tree.repository_id, _force_current_tree=True)
|
||||||
|
|
||||||
|
# create repository tree
|
||||||
|
target_tree.tree_create()
|
||||||
|
# perform migration
|
||||||
|
TreeMigrate.tree_move(current_tree, target_tree)
|
||||||
|
TreeMigrate.symlinks_fix(target_tree)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_service_tree_migrate_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for tree migration subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("service-tree-migrate", help="migrate repository tree",
|
||||||
|
description="migrate repository tree between versions")
|
||||||
|
parser.set_defaults(lock=None, quiet=True, report=False)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def symlinks_fix(paths: RepositoryPaths) -> None:
|
||||||
|
"""
|
||||||
|
fix package archive symlinks
|
||||||
|
|
||||||
|
Args:
|
||||||
|
paths(RepositoryPaths): new repository paths
|
||||||
|
"""
|
||||||
|
archives = {path.name: path for path in walk(paths.archive)}
|
||||||
|
for symlink in walk(paths.repository):
|
||||||
|
if symlink.exists(): # no need to check for symlinks as we have just walked through the tree
|
||||||
|
continue
|
||||||
|
if (source_archive := archives.get(symlink.name)) is not None:
|
||||||
|
symlink.unlink()
|
||||||
|
symlink_relative(symlink, source_archive)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def tree_move(from_tree: RepositoryPaths, to_tree: RepositoryPaths) -> None:
|
||||||
|
"""
|
||||||
|
move files between trees. Trees must be created in advance
|
||||||
|
|
||||||
|
Args:
|
||||||
|
from_tree(RepositoryPaths): old repository tree
|
||||||
|
to_tree(RepositoryPaths): new repository tree
|
||||||
|
"""
|
||||||
|
# we don't care about devtools chroot
|
||||||
|
for attribute in (
|
||||||
|
RepositoryPaths.packages,
|
||||||
|
RepositoryPaths.pacman,
|
||||||
|
RepositoryPaths.repository,
|
||||||
|
):
|
||||||
|
attribute.fget(from_tree).rename(attribute.fget(to_tree)) # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
arguments = [_set_service_tree_migrate_parser]
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
from ahriman.models.result import Result
|
||||||
|
|
||||||
|
|
||||||
|
class Triggers(Handler):
|
||||||
|
"""
|
||||||
|
triggers handlers
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=report)
|
||||||
|
if args.trigger:
|
||||||
|
loader = application.repository.triggers
|
||||||
|
loader.triggers = [loader.load_trigger(trigger, repository_id, configuration) for trigger in args.trigger]
|
||||||
|
application.on_start()
|
||||||
|
application.on_result(Result())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_repo_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for report subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("repo-report", aliases=["report"], help="generate report",
|
||||||
|
description="generate repository report according to current settings",
|
||||||
|
epilog="Create and/or update repository report as configured.")
|
||||||
|
parser.set_defaults(trigger=["ahriman.core.report.ReportTrigger"])
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_repo_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for repository sync subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("repo-sync", aliases=["sync"], help="sync repository",
|
||||||
|
description="sync repository files to remote server according to current settings",
|
||||||
|
epilog="Synchronize the repository to remote services as configured.")
|
||||||
|
parser.set_defaults(trigger=["ahriman.core.upload.UploadTrigger"])
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_repo_triggers_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for repository triggers subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("repo-triggers", help="run triggers",
|
||||||
|
description="run triggers on empty build result as configured by settings")
|
||||||
|
parser.add_argument("trigger", help="instead of running all triggers as set by configuration, just process "
|
||||||
|
"specified ones in order of mention", nargs="*")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [
|
||||||
|
_set_repo_report_parser,
|
||||||
|
_set_repo_sync_parser,
|
||||||
|
_set_repo_triggers_parser,
|
||||||
|
]
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import StringPrinter
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class UnsafeCommands(Handler):
|
||||||
|
"""
|
||||||
|
unsafe command help parser
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
parser = args.parser()
|
||||||
|
unsafe_commands = UnsafeCommands.get_unsafe_commands(parser)
|
||||||
|
if args.subcommand:
|
||||||
|
UnsafeCommands.check_unsafe(args.subcommand, unsafe_commands, parser)
|
||||||
|
else:
|
||||||
|
for command in unsafe_commands:
|
||||||
|
StringPrinter(command)(verbose=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for listing unsafe commands
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("help-commands-unsafe", help="list unsafe commands",
|
||||||
|
description="list unsafe commands as defined in default args")
|
||||||
|
parser.add_argument("subcommand",
|
||||||
|
help="instead of showing commands, just test command line for unsafe subcommand "
|
||||||
|
"and return 0 in case if command is safe and 1 otherwise", nargs="*")
|
||||||
|
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_unsafe(command: list[str], unsafe_commands: list[str], parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
check if command is unsafe
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command(str): command to check
|
||||||
|
unsafe_commands(list[str]): list of unsafe commands
|
||||||
|
parser(argparse.ArgumentParser): generated argument parser
|
||||||
|
"""
|
||||||
|
args = parser.parse_args(command)
|
||||||
|
UnsafeCommands.check_status(True, args.command not in unsafe_commands)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_unsafe_commands(parser: argparse.ArgumentParser) -> list[str]:
|
||||||
|
"""
|
||||||
|
extract unsafe commands from argument parser
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parser(argparse.ArgumentParser): generated argument parser
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[str]: list of commands with default unsafe flag
|
||||||
|
"""
|
||||||
|
# should never fail
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
subparser = next((action for action in parser._actions if isinstance(action, argparse._SubParsersAction)), None)
|
||||||
|
actions = subparser.choices if subparser is not None else {}
|
||||||
|
return sorted(action_name for action_name, action in actions.items() if action.get_default("unsafe"))
|
||||||
|
|
||||||
|
arguments = [_set_help_commands_unsafe_parser]
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.utils import extract_user
|
||||||
|
from ahriman.models.packagers import Packagers
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Update(Handler):
|
||||||
|
"""
|
||||||
|
package update handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
||||||
|
application.on_start()
|
||||||
|
Update.perform_action(application, args)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for repository check subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("repo-check", aliases=["check"], help="check for updates",
|
||||||
|
description="check for packages updates. Same as repo-update --dry-run --no-manual")
|
||||||
|
parser.add_argument("package", help="filter check by package base", nargs="*")
|
||||||
|
parser.add_argument("--changes", help="calculate changes from the latest known commit if available",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--check-files", help="enable or disable checking of broken dependencies "
|
||||||
|
"(e.g. dynamically linked libraries or modules directories)",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--vcs", help="fetch actual version of VCS packages",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
|
||||||
|
"-yy to force refresh even if up to date",
|
||||||
|
action="count", default=False)
|
||||||
|
parser.set_defaults(aur=True, dependencies=False, dry_run=True, increment=False, local=True, manual=False,
|
||||||
|
username=None)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for repository update subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("repo-update", aliases=["update"], help="update packages",
|
||||||
|
description="check for packages updates and run build process if requested")
|
||||||
|
parser.add_argument("package", help="filter check by package base", nargs="*")
|
||||||
|
parser.add_argument("--aur", help="enable or disable checking for AUR updates",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--changes", help="calculate changes from the latest known commit if available. "
|
||||||
|
"Only applicable in dry run mode",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--check-files", help="enable or disable checking of broken dependencies "
|
||||||
|
"(e.g. dynamically linked libraries or modules directories)",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--dependencies", help="process missing package dependencies",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--dry-run", help="just perform check for updates, same as check command",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--increment", help="increment package release (pkgrel) on duplicate",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--local", help="enable or disable checking of local packages for updates",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--manual", help="include or exclude manual updates",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
|
||||||
|
parser.add_argument("--vcs", help="fetch actual version of VCS packages",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
|
||||||
|
"-yy to force refresh even if up to date",
|
||||||
|
action="count", default=False)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def log_fn(application: Application, dry_run: bool) -> Callable[[str], None]:
|
||||||
|
"""
|
||||||
|
package updates log function
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
dry_run(bool): do not perform update itself
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Callable[[str], None]: in case if dry_run is set it will return print, logger otherwise
|
||||||
|
"""
|
||||||
|
def inner(line: str) -> None:
|
||||||
|
return print(line) if dry_run else application.logger.info(line) # pylint: disable=bad-builtin
|
||||||
|
return inner
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def perform_action(application: Application, args: argparse.Namespace) -> None:
|
||||||
|
"""
|
||||||
|
perform update action
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
"""
|
||||||
|
packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs,
|
||||||
|
check_files=args.check_files)
|
||||||
|
if args.changes: # generate changes if requested
|
||||||
|
application.changes(packages)
|
||||||
|
|
||||||
|
if args.dry_run: # exit from application if no build requested
|
||||||
|
Update.check_status(args.exit_code, packages) # status code check
|
||||||
|
return
|
||||||
|
|
||||||
|
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
|
||||||
|
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
|
||||||
|
|
||||||
|
application.print_updates(packages, log_fn=application.logger.info)
|
||||||
|
result = application.update(packages, packagers, bump_pkgrel=args.increment)
|
||||||
|
Update.check_status(args.exit_code, not result.is_empty)
|
||||||
|
|
||||||
|
arguments = [
|
||||||
|
_set_repo_check_parser,
|
||||||
|
_set_repo_update_parser,
|
||||||
|
]
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.database import SQLite
|
||||||
|
from ahriman.core.exceptions import PasswordError
|
||||||
|
from ahriman.core.formatters import UserPrinter
|
||||||
|
from ahriman.core.utils import enum_values
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
from ahriman.models.user import User
|
||||||
|
from ahriman.models.user_access import UserAccess
|
||||||
|
|
||||||
|
|
||||||
|
class Users(Handler):
|
||||||
|
"""
|
||||||
|
user management handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
database = SQLite.load(configuration)
|
||||||
|
|
||||||
|
match args.action:
|
||||||
|
case Action.Update:
|
||||||
|
user = Users.user_create(args)
|
||||||
|
# if password is left blank we are not going to require salt to be set
|
||||||
|
salt = configuration.get("auth", "salt", fallback="") if user.password else ""
|
||||||
|
database.user_update(user.hash_password(salt))
|
||||||
|
case Action.List:
|
||||||
|
users = database.user_list(args.username, args.role)
|
||||||
|
for user in users:
|
||||||
|
UserPrinter(user)(verbose=True)
|
||||||
|
Users.check_status(args.exit_code, users)
|
||||||
|
case Action.Remove:
|
||||||
|
database.user_remove(args.username)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for create user subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("user-add", help="create or update user",
|
||||||
|
description="update user for web services with the given password and role. "
|
||||||
|
"In case if password was not entered it will be asked interactively")
|
||||||
|
parser.add_argument("username", help="username for web service")
|
||||||
|
parser.add_argument("--key", help="optional PGP key used by this user. The private key must be imported")
|
||||||
|
parser.add_argument("--packager", help="optional packager id used for build process in form of "
|
||||||
|
"`Name Surname <mail@example.com>`")
|
||||||
|
parser.add_argument(
|
||||||
|
"-p", "--password", help="user password. Blank password will be treated as empty password, "
|
||||||
|
"which is in particular must be used for OAuth2 authorization type.")
|
||||||
|
parser.add_argument("-R", "--role", help="user access level",
|
||||||
|
type=UserAccess, choices=enum_values(UserAccess), default=UserAccess.Read)
|
||||||
|
parser.set_defaults(action=Action.Update, architecture="", exit_code=False, lock=None, quiet=True,
|
||||||
|
report=False, repository="")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for user list subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("user-list", help="user known users and their access",
|
||||||
|
description="list users from the user mapping and their roles")
|
||||||
|
parser.add_argument("username", help="filter users by username", nargs="?")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("-R", "--role", help="filter users by role", type=UserAccess,
|
||||||
|
choices=enum_values(UserAccess))
|
||||||
|
parser.set_defaults(action=Action.List, architecture="", lock=None, quiet=True, report=False, repository="",
|
||||||
|
unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for user removal subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("user-remove", help="remove user",
|
||||||
|
description="remove user from the user mapping and update the configuration")
|
||||||
|
parser.add_argument("username", help="username for web service")
|
||||||
|
parser.set_defaults(action=Action.Remove, architecture="", exit_code=False, lock=None, quiet=True,
|
||||||
|
report=False, repository="")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def user_create(args: argparse.Namespace) -> User:
|
||||||
|
"""
|
||||||
|
create user descriptor from arguments
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
User: built user descriptor
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PasswordError: password input is invalid
|
||||||
|
"""
|
||||||
|
def read_password() -> str:
|
||||||
|
first_password = getpass.getpass()
|
||||||
|
second_password = getpass.getpass("Repeat password: ")
|
||||||
|
if first_password != second_password:
|
||||||
|
raise PasswordError("passwords don't match")
|
||||||
|
return first_password
|
||||||
|
|
||||||
|
password = args.password
|
||||||
|
if password is None:
|
||||||
|
password = read_password()
|
||||||
|
|
||||||
|
return User(username=args.username, password=password, access=args.role,
|
||||||
|
packager_id=args.packager, key=args.key)
|
||||||
|
|
||||||
|
arguments = [
|
||||||
|
_set_user_add_parser,
|
||||||
|
_set_user_list_parser,
|
||||||
|
_set_user_remove_parser,
|
||||||
|
]
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.configuration.schema import CONFIGURATION_SCHEMA, ConfigurationSchema
|
||||||
|
from ahriman.core.exceptions import ExtensionError
|
||||||
|
from ahriman.core.formatters import ValidationPrinter
|
||||||
|
from ahriman.core.triggers import TriggerLoader
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Validate(Handler):
|
||||||
|
"""
|
||||||
|
configuration validator handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
from ahriman.core.configuration.validator import Validator
|
||||||
|
|
||||||
|
schema = Validate.schema(configuration)
|
||||||
|
validator = Validator(configuration=configuration, schema=schema)
|
||||||
|
|
||||||
|
if validator.validate(configuration.dump()):
|
||||||
|
return # no errors found
|
||||||
|
for node, errors in validator.errors.items():
|
||||||
|
ValidationPrinter(node, errors)(verbose=True)
|
||||||
|
|
||||||
|
# as we reach this part it means that we always have errors
|
||||||
|
Validate.check_status(args.exit_code, False)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_service_config_validate_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for config validation subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("service-config-validate", aliases=["config-validate", "repo-config-validate"],
|
||||||
|
help="validate system configuration",
|
||||||
|
description="validate configuration and print found errors")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if configuration is invalid",
|
||||||
|
action="store_true")
|
||||||
|
parser.set_defaults(lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def schema(configuration: Configuration) -> ConfigurationSchema:
|
||||||
|
"""
|
||||||
|
get schema with triggers
|
||||||
|
|
||||||
|
Args:
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ConfigurationSchema: configuration validation schema
|
||||||
|
"""
|
||||||
|
root = copy.deepcopy(CONFIGURATION_SCHEMA)
|
||||||
|
|
||||||
|
# create trigger loader instance
|
||||||
|
loader = TriggerLoader()
|
||||||
|
triggers = loader.selected_triggers(configuration) + loader.known_triggers(configuration)
|
||||||
|
|
||||||
|
for trigger in set(triggers):
|
||||||
|
try:
|
||||||
|
trigger_class = loader.load_trigger_class(trigger)
|
||||||
|
except ExtensionError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# default settings if any
|
||||||
|
for schema_name, schema in trigger_class.configuration_schema(None).items():
|
||||||
|
erased = Validate.schema_erase_required(copy.deepcopy(schema))
|
||||||
|
root[schema_name] = Validate.schema_merge(root.get(schema_name, {}), erased)
|
||||||
|
|
||||||
|
# settings according to enabled triggers
|
||||||
|
for schema_name, schema in trigger_class.configuration_schema(configuration).items():
|
||||||
|
root[schema_name] = Validate.schema_merge(root.get(schema_name, {}), copy.deepcopy(schema))
|
||||||
|
|
||||||
|
return root
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def schema_erase_required(schema: ConfigurationSchema) -> ConfigurationSchema:
|
||||||
|
"""
|
||||||
|
recursively remove required field from supplied cerberus schema
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schema(ConfigurationSchema): source schema from which required field must be removed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ConfigurationSchema: schema without required fields. Note, that source schema will be modified in-place
|
||||||
|
"""
|
||||||
|
schema.pop("required", None)
|
||||||
|
for value in filter(lambda v: isinstance(v, dict), schema.values()):
|
||||||
|
Validate.schema_erase_required(value)
|
||||||
|
return schema
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def schema_merge(source: dict[str, Any], schema: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
merge child schema into source. In case if source already contains values, new keys will be added
|
||||||
|
(possibly with overrides - in case if such key already set also)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source(dict[str, Any]): source (current) schema into which will be merged
|
||||||
|
schema(dict[str, Any]): new schema to be merged
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, Any]: schema with added elements from source schema if they were set before and not presented
|
||||||
|
in the new one. Note, that schema will be modified in-place
|
||||||
|
"""
|
||||||
|
for key, value in source.items():
|
||||||
|
if key not in schema:
|
||||||
|
schema[key] = value # new key found, just add it as is
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
# value is dictionary, so we need to go deeper
|
||||||
|
Validate.schema_merge(value, schema[key])
|
||||||
|
|
||||||
|
return schema
|
||||||
|
|
||||||
|
arguments = [_set_service_config_validate_parser]
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from importlib import metadata
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
from ahriman import __version__
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import VersionPrinter
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Versions(Handler):
|
||||||
|
"""
|
||||||
|
version handler
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
PEP423_PACKAGE_NAME(re.Pattern[str]): (class attribute) special regex for valid PEP423 package name
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||||
|
PEP423_PACKAGE_NAME: ClassVar[re.Pattern[str]] = re.compile(r"^[A-Za-z0-9._-]+")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
VersionPrinter(f"Module version {__version__}",
|
||||||
|
{"Python": sys.version})(verbose=False, separator=" ")
|
||||||
|
packages = Versions.package_dependencies("ahriman")
|
||||||
|
VersionPrinter("Installed packages", dict(packages))(verbose=False, separator=" ")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_help_version_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for version subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("help-version", aliases=["version"], help="application version",
|
||||||
|
description="print application and its dependencies versions")
|
||||||
|
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def package_dependencies(root: str) -> Iterator[tuple[str, str]]:
|
||||||
|
"""
|
||||||
|
extract list of ahriman package dependencies installed into system with their versions
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(str): root package name
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
tuple[str, str]: map of installed dependency to its version
|
||||||
|
"""
|
||||||
|
def dependencies_by_key(key: str) -> Iterator[str]:
|
||||||
|
# in importlib it returns requires in the following format
|
||||||
|
# ["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"]
|
||||||
|
try:
|
||||||
|
requires = metadata.requires(key)
|
||||||
|
except metadata.PackageNotFoundError:
|
||||||
|
return
|
||||||
|
for entry in requires or []:
|
||||||
|
yield from Versions.PEP423_PACKAGE_NAME.findall(entry)
|
||||||
|
|
||||||
|
keys: list[str] = []
|
||||||
|
portion = set(dependencies_by_key(root))
|
||||||
|
while portion:
|
||||||
|
keys.extend(portion)
|
||||||
|
portion = {
|
||||||
|
key
|
||||||
|
for key in sum((list(dependencies_by_key(key)) for key in portion), start=[])
|
||||||
|
if key not in keys
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in keys:
|
||||||
|
try:
|
||||||
|
distribution = metadata.distribution(key)
|
||||||
|
yield distribution.name, distribution.version
|
||||||
|
except metadata.PackageNotFoundError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
arguments = [_set_help_version_parser]
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
|
||||||
|
class _HelpFormatter(argparse.ArgumentDefaultsHelpFormatter):
|
||||||
|
"""
|
||||||
|
:class:`argparse.ArgumentDefaultsHelpFormatter` extension which keeps new lines in help messages
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, prog: str) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
prog(str): application name
|
||||||
|
"""
|
||||||
|
argparse.ArgumentDefaultsHelpFormatter.__init__(self, prog, width=120)
|
||||||
|
self._whitespace_matcher = re.compile(r"[ \t]+")
|
||||||
|
|
||||||
|
def _fill_text(self, text: str, width: int, indent: str) -> str:
|
||||||
|
"""
|
||||||
|
implementation of the protected method to format text. Format text, keeping new lines
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text(str): text to format
|
||||||
|
width(int): maximal text width
|
||||||
|
indent(str): indentation string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: formatted text
|
||||||
|
"""
|
||||||
|
text = self._whitespace_matcher.sub(" ", text).strip()
|
||||||
|
return "\n".join([
|
||||||
|
textwrap.fill(line, width, initial_indent=indent, subsequent_indent=indent)
|
||||||
|
for line in text.splitlines()
|
||||||
|
])
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from code import InteractiveConsole
|
||||||
|
from importlib.util import find_spec
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class InteractiveShell(InteractiveConsole):
|
||||||
|
"""
|
||||||
|
wrapper around :class:`code.InteractiveConsole` to pass :func:`interact()` to IPython shell
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def has_ipython() -> bool:
|
||||||
|
"""
|
||||||
|
check if IPython shell is available
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: ``True`` if IPython shell is available, ``False`` otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return find_spec("IPython.terminal.embed") is not None
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def interact(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""
|
||||||
|
pass controller to IPython shell
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*args(Any): positional arguments
|
||||||
|
**kwargs(Any): keyword arguments
|
||||||
|
"""
|
||||||
|
if self.has_ipython():
|
||||||
|
from IPython.terminal.embed import InteractiveShellEmbed
|
||||||
|
|
||||||
|
shell = InteractiveShellEmbed(user_ns=self.locals) # type: ignore[no-untyped-call]
|
||||||
|
shell.show_banner() # type: ignore[no-untyped-call]
|
||||||
|
shell.interact() # type: ignore[no-untyped-call]
|
||||||
|
else:
|
||||||
|
# fallback to default
|
||||||
|
import readline # pylint: disable=unused-import
|
||||||
|
InteractiveConsole.interact(self, *args, **kwargs)
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
import fcntl
|
||||||
|
import os
|
||||||
|
|
||||||
|
from io import TextIOWrapper
|
||||||
|
from pathlib import Path
|
||||||
|
from types import TracebackType
|
||||||
|
from typing import Literal, Self
|
||||||
|
|
||||||
|
from ahriman import __version__
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.exceptions import DuplicateRunError
|
||||||
|
from ahriman.core.log import LazyLogging
|
||||||
|
from ahriman.core.status import Client
|
||||||
|
from ahriman.core.utils import check_user
|
||||||
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
from ahriman.models.waiter import Waiter
|
||||||
|
|
||||||
|
|
||||||
|
class Lock(LazyLogging):
|
||||||
|
"""
|
||||||
|
wrapper for application lock file. Credits for idea to https://github.com/bmhatfield/python-pidfile.git
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
force(bool): remove lock file on start if any
|
||||||
|
path(Path): path to lock file if any
|
||||||
|
reporter(Client): build status reporter instance
|
||||||
|
paths(RepositoryPaths): repository paths instance
|
||||||
|
unsafe(bool): skip user check
|
||||||
|
wait_timeout(int): wait in seconds until lock will free
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Instance of this class except for controlling file-based lock is also required for basic applications checks.
|
||||||
|
The common flow is to create instance in ``with`` block and handle exceptions after all::
|
||||||
|
|
||||||
|
>>> from ahriman.core.configuration import Configuration
|
||||||
|
>>> from ahriman.models.repository_id import RepositoryId
|
||||||
|
>>>
|
||||||
|
>>> configuration = Configuration()
|
||||||
|
>>> try:
|
||||||
|
>>> with Lock(args, RepositoryId("x86_64", "aur"), configuration):
|
||||||
|
>>> do_something()
|
||||||
|
>>> except Exception as exception:
|
||||||
|
>>> handle_exceptions(exception)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
"""
|
||||||
|
self.path: Path | None = None
|
||||||
|
if args.lock is not None:
|
||||||
|
self.path = args.lock
|
||||||
|
if not repository_id.is_empty:
|
||||||
|
self.path = self.path.with_stem(f"{args.lock.stem}_{repository_id.id}")
|
||||||
|
if not self.path.is_absolute():
|
||||||
|
# prepend full path to the lock file
|
||||||
|
self.path = Path("/") / "run" / "ahriman" / self.path
|
||||||
|
self._pid_file: TextIOWrapper | None = None
|
||||||
|
|
||||||
|
self.force: bool = args.force
|
||||||
|
self.unsafe: bool = args.unsafe
|
||||||
|
self.wait_timeout: int = args.wait_timeout
|
||||||
|
|
||||||
|
self.paths = configuration.repository_paths
|
||||||
|
self.reporter = Client.load(repository_id, configuration, report=args.report)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def perform_lock(fd: int) -> bool:
|
||||||
|
"""
|
||||||
|
perform file lock
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fd(int): file descriptor:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: ``True`` in case if file is locked and ``False`` otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _open(self) -> None:
|
||||||
|
"""
|
||||||
|
create lock file
|
||||||
|
"""
|
||||||
|
if self.path is None:
|
||||||
|
return
|
||||||
|
self._pid_file = self.path.open("a+", encoding="utf8")
|
||||||
|
|
||||||
|
def _watch(self) -> bool:
|
||||||
|
"""
|
||||||
|
watch until lock disappear
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: ``True`` in case if file is locked and ``False`` otherwise
|
||||||
|
"""
|
||||||
|
# there are reasons why we are not using inotify here. First of all, if we would use it, it would bring to
|
||||||
|
# race conditions because multiple processes will be notified at the same time. Secondly, it is good library,
|
||||||
|
# but platform-specific, and we only need to check if file exists
|
||||||
|
if self._pid_file is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
waiter = Waiter(self.wait_timeout)
|
||||||
|
return bool(waiter.wait(lambda fd: not self.perform_lock(fd), self._pid_file.fileno()))
|
||||||
|
|
||||||
|
def _write(self, *, is_locked: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
write pid to the lock file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
is_locked(bool, optional): indicates if file was already locked or not (Default value = False)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
DuplicateRunError: if it cannot lock PID file
|
||||||
|
"""
|
||||||
|
if self._pid_file is None:
|
||||||
|
return
|
||||||
|
if not is_locked:
|
||||||
|
if not self.perform_lock(self._pid_file.fileno()):
|
||||||
|
raise DuplicateRunError
|
||||||
|
|
||||||
|
self._pid_file.seek(0) # reset position and remove file content if any
|
||||||
|
self._pid_file.truncate()
|
||||||
|
|
||||||
|
self._pid_file.write(str(os.getpid())) # write current pid
|
||||||
|
self._pid_file.flush() # flush data to disk
|
||||||
|
|
||||||
|
self._pid_file.seek(0) # reset position again
|
||||||
|
|
||||||
|
def check_user(self) -> None:
|
||||||
|
"""
|
||||||
|
check if current user is actually owner of ahriman root
|
||||||
|
"""
|
||||||
|
check_user(self.paths.root, unsafe=self.unsafe)
|
||||||
|
self.paths.tree_create()
|
||||||
|
|
||||||
|
def check_version(self) -> None:
|
||||||
|
"""
|
||||||
|
check web server version
|
||||||
|
"""
|
||||||
|
status = self.reporter.status_get()
|
||||||
|
if status.version is not None and status.version != __version__:
|
||||||
|
self.logger.warning("status watcher version mismatch, our %s, their %s",
|
||||||
|
__version__, status.version)
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""
|
||||||
|
remove lock file
|
||||||
|
"""
|
||||||
|
if self._pid_file is not None: # close file descriptor
|
||||||
|
try:
|
||||||
|
self._pid_file.close()
|
||||||
|
except IOError:
|
||||||
|
pass # suppress any IO errors occur
|
||||||
|
if self.path is not None: # remove lock file
|
||||||
|
self.path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
def lock(self) -> None:
|
||||||
|
"""
|
||||||
|
create pid file
|
||||||
|
"""
|
||||||
|
if self.force: # remove lock if force flag is set
|
||||||
|
self.clear()
|
||||||
|
self._open()
|
||||||
|
is_locked = self._watch()
|
||||||
|
self._write(is_locked=is_locked)
|
||||||
|
|
||||||
|
def __enter__(self) -> Self:
|
||||||
|
"""
|
||||||
|
default workflow is the following:
|
||||||
|
|
||||||
|
#. Check user UID
|
||||||
|
#. Check web status watcher status
|
||||||
|
#. Open lock file
|
||||||
|
#. Wait for lock file to be free
|
||||||
|
#. Write current PID to the lock file
|
||||||
|
#. Report to status page if enabled
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Self: always instance of self
|
||||||
|
"""
|
||||||
|
self.check_user()
|
||||||
|
self.check_version()
|
||||||
|
self.lock()
|
||||||
|
self.reporter.status_update(BuildStatusEnum.Building)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type: type[Exception] | None, exc_val: Exception | None,
|
||||||
|
exc_tb: TracebackType) -> Literal[False]:
|
||||||
|
"""
|
||||||
|
remove lock file when done
|
||||||
|
|
||||||
|
Args:
|
||||||
|
exc_type(type[Exception] | None): exception type name if any
|
||||||
|
exc_val(Exception | None): exception raised if any
|
||||||
|
exc_tb(TracebackType): exception traceback if any
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Literal[False]: always ``False`` (do not suppress any exception)
|
||||||
|
"""
|
||||||
|
self.clear()
|
||||||
|
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
|
||||||
|
self.reporter.status_update(status)
|
||||||
|
return False
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from contextvars import ContextVar
|
||||||
|
from typing import Any, TypeVar
|
||||||
|
|
||||||
|
from ahriman.models.context_key import ContextKey
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
class _Context:
|
||||||
|
"""
|
||||||
|
simple ahriman global context implementation
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
""""""
|
||||||
|
self._content: dict[str, Any] = {}
|
||||||
|
|
||||||
|
def get(self, key: ContextKey[T] | type[T]) -> T:
|
||||||
|
"""
|
||||||
|
get value for the specified key
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key(ContextKey[T] | type[T]): context key name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
T: value associated with the key
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: in case if the specified context variable was not found
|
||||||
|
ValueError: in case if type of value is not an instance of specified return type
|
||||||
|
"""
|
||||||
|
if not isinstance(key, ContextKey):
|
||||||
|
key = ContextKey.from_type(key)
|
||||||
|
|
||||||
|
if key.key not in self._content:
|
||||||
|
raise KeyError(key.key)
|
||||||
|
value = self._content[key.key]
|
||||||
|
if not isinstance(value, key.return_type):
|
||||||
|
raise ValueError(f"Value {value} is not an instance of {key.return_type}")
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def set(self, key: ContextKey[T] | type[T], value: T) -> None:
|
||||||
|
"""
|
||||||
|
set value for the specified key
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key(ContextKey[T] | type[T]): context key name
|
||||||
|
value(T): context value associated with the specified key
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: in case if the specified context variable already exists
|
||||||
|
ValueError: in case if type of value is not an instance of specified return type
|
||||||
|
"""
|
||||||
|
if not isinstance(key, ContextKey):
|
||||||
|
key = ContextKey.from_type(key)
|
||||||
|
|
||||||
|
if key.key in self._content:
|
||||||
|
raise KeyError(key.key)
|
||||||
|
if not isinstance(value, key.return_type):
|
||||||
|
raise ValueError(f"Value {value} is not an instance of {key.return_type}")
|
||||||
|
|
||||||
|
self._content[key.key] = value
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[str]:
|
||||||
|
"""
|
||||||
|
iterate over keys in local storage
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: context key iterator
|
||||||
|
"""
|
||||||
|
return iter(self._content)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
"""
|
||||||
|
get count of the context variables set
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: count of stored context variables
|
||||||
|
"""
|
||||||
|
return len(self._content)
|
||||||
|
|
||||||
|
|
||||||
|
context = ContextVar("context", default=_Context())
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#
|
#
|
||||||
# Copyright (c) 2021 Evgenii Alekseev.
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
#
|
#
|
||||||
# This file is part of ahriman
|
# This file is part of ahriman
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import itertools
|
||||||
|
import shutil
|
||||||
|
import tarfile
|
||||||
|
|
||||||
|
from collections.abc import Iterable, Iterator
|
||||||
|
from functools import cached_property
|
||||||
|
from pathlib import Path
|
||||||
|
from pyalpm import DB, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found]
|
||||||
|
from string import Template
|
||||||
|
|
||||||
|
from ahriman.core.alpm.pacman_database import PacmanDatabase
|
||||||
|
from ahriman.core.alpm.pacman_handle import PacmanHandle
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.log import LazyLogging
|
||||||
|
from ahriman.core.utils import trim_package
|
||||||
|
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Pacman(LazyLogging):
|
||||||
|
"""
|
||||||
|
alpm wrapper
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
refresh_database(PacmanSynchronization): synchronize local cache to remote
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
repository_paths(RepositoryPaths): repository paths instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
refresh_database: PacmanSynchronization) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
refresh_database(PacmanSynchronization): synchronize local cache to remote
|
||||||
|
"""
|
||||||
|
self.configuration = configuration
|
||||||
|
self.repository_id = repository_id
|
||||||
|
self.repository_paths = configuration.repository_paths
|
||||||
|
|
||||||
|
self.refresh_database = refresh_database
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def handle(self) -> PacmanHandle:
|
||||||
|
"""
|
||||||
|
pyalpm handle
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PacmanHandle: generated pyalpm handle instance
|
||||||
|
"""
|
||||||
|
return self.__create_handle(refresh_database=self.refresh_database)
|
||||||
|
|
||||||
|
def __create_handle(self, *, refresh_database: PacmanSynchronization) -> PacmanHandle:
|
||||||
|
"""
|
||||||
|
create lazy handle function
|
||||||
|
|
||||||
|
Args:
|
||||||
|
refresh_database(PacmanSynchronization): synchronize local cache to remote
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PacmanHandle: fully initialized pacman handle
|
||||||
|
"""
|
||||||
|
pacman_root = self.configuration.getpath("alpm", "database")
|
||||||
|
use_ahriman_cache = self.configuration.getboolean("alpm", "use_ahriman_cache")
|
||||||
|
|
||||||
|
database_path = self.repository_paths.pacman if use_ahriman_cache else pacman_root
|
||||||
|
root = self.configuration.getpath("alpm", "root")
|
||||||
|
handle = PacmanHandle(str(root), str(database_path))
|
||||||
|
|
||||||
|
for repository in self.configuration.getlist("alpm", "repositories"):
|
||||||
|
database = self.database_init(handle, repository, self.repository_id.architecture)
|
||||||
|
self.database_copy(handle, database, pacman_root, use_ahriman_cache=use_ahriman_cache)
|
||||||
|
|
||||||
|
# install repository database too (without copying)
|
||||||
|
self.database_init(handle, self.repository_id.name, self.repository_id.architecture)
|
||||||
|
|
||||||
|
if use_ahriman_cache and refresh_database:
|
||||||
|
self.database_sync(handle, force=refresh_database == PacmanSynchronization.Force)
|
||||||
|
|
||||||
|
return handle
|
||||||
|
|
||||||
|
def database_copy(self, handle: PacmanHandle, database: DB, pacman_root: Path, *, use_ahriman_cache: bool) -> None:
|
||||||
|
"""
|
||||||
|
copy database from the operating system root to the ahriman local home
|
||||||
|
|
||||||
|
Args:
|
||||||
|
handle(PacmanHandle): pacman handle which will be used for database copying
|
||||||
|
database(DB): pacman database instance to be copied
|
||||||
|
pacman_root(Path): operating system pacman root
|
||||||
|
use_ahriman_cache(bool): use local ahriman cache instead of system one
|
||||||
|
"""
|
||||||
|
def repository_database(root: Path) -> Path:
|
||||||
|
return root / "sync" / f"{database.name}.db"
|
||||||
|
|
||||||
|
if not use_ahriman_cache:
|
||||||
|
return
|
||||||
|
|
||||||
|
# copy root database if no local copy found
|
||||||
|
pacman_db_path = Path(handle.dbpath)
|
||||||
|
if not pacman_db_path.is_dir():
|
||||||
|
return # root directory does not exist yet
|
||||||
|
dst = repository_database(pacman_db_path)
|
||||||
|
if dst.is_file():
|
||||||
|
return # file already exists, do not copy
|
||||||
|
dst.parent.mkdir(mode=0o755, exist_ok=True) # create sync directory if it doesn't exist
|
||||||
|
|
||||||
|
src = repository_database(pacman_root)
|
||||||
|
if not src.is_file():
|
||||||
|
self.logger.warning("repository %s is set to be used, however, no working copy was found", database.name)
|
||||||
|
return # database for some reason deos not exist
|
||||||
|
|
||||||
|
self.logger.info("copy pacman database %s from operating system root to ahriman's home %s", src, dst)
|
||||||
|
with self.repository_paths.preserve_owner():
|
||||||
|
shutil.copy(src, dst)
|
||||||
|
|
||||||
|
def database_init(self, handle: PacmanHandle, repository: str, architecture: str) -> DB:
|
||||||
|
"""
|
||||||
|
create database instance from pacman handler and set its properties
|
||||||
|
|
||||||
|
Args:
|
||||||
|
handle(PacmanHandle): pacman handle which will be used for database initializing
|
||||||
|
repository(str): pacman repository name (e.g. core)
|
||||||
|
architecture(str): repository architecture
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DB: loaded pacman database instance
|
||||||
|
"""
|
||||||
|
self.logger.info("loading pacman database %s", repository)
|
||||||
|
database: DB = handle.register_syncdb(repository, SIG_DATABASE_OPTIONAL | SIG_PACKAGE_OPTIONAL)
|
||||||
|
|
||||||
|
if repository != self.repository_id.name:
|
||||||
|
mirror = self.configuration.get("alpm", "mirror")
|
||||||
|
# replace variables in mirror address
|
||||||
|
variables = {
|
||||||
|
"arch": architecture,
|
||||||
|
"repo": repository,
|
||||||
|
}
|
||||||
|
server = Template(mirror).safe_substitute(variables)
|
||||||
|
else:
|
||||||
|
# special case, same database, use local storage instead
|
||||||
|
server = f"file://{self.repository_paths.repository}"
|
||||||
|
|
||||||
|
database.servers = [server]
|
||||||
|
|
||||||
|
return database
|
||||||
|
|
||||||
|
def database_sync(self, handle: PacmanHandle, *, force: bool) -> None:
|
||||||
|
"""
|
||||||
|
sync local database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
handle(PacmanHandle): pacman handle which will be used for database sync
|
||||||
|
force(bool): force database synchronization (same as ``pacman -Syy``)
|
||||||
|
"""
|
||||||
|
self.logger.info("refresh ahriman's home pacman database (force refresh %s)", force)
|
||||||
|
transaction = handle.init_transaction()
|
||||||
|
for database in handle.get_syncdbs():
|
||||||
|
PacmanDatabase(database, self.configuration).sync(force=force)
|
||||||
|
transaction.release()
|
||||||
|
|
||||||
|
def files(self, packages: Iterable[str]) -> dict[str, set[str]]:
|
||||||
|
"""
|
||||||
|
extract list of known packages from the databases
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packages(Iterable[str]): filter by package names
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, set[str]]: map of package name to its list of files
|
||||||
|
"""
|
||||||
|
def extract(tar: tarfile.TarFile, versions: dict[str, str]) -> Iterator[tuple[str, set[str]]]:
|
||||||
|
for package_name, version in versions.items():
|
||||||
|
path = Path(f"{package_name}-{version}") / "files"
|
||||||
|
try:
|
||||||
|
content = tar.extractfile(str(path))
|
||||||
|
except KeyError:
|
||||||
|
# in case if database and its files has been desync somehow, the extractfile will raise
|
||||||
|
# KeyError because the entry doesn't exist
|
||||||
|
content = None
|
||||||
|
if content is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# this is just array of files, however, the directories are with trailing slash,
|
||||||
|
# which previously has been removed by the conversion to ``pathlib.Path``
|
||||||
|
files = {filename.decode("utf8").rstrip().removesuffix("/") for filename in content.readlines()}
|
||||||
|
yield package_name, files
|
||||||
|
|
||||||
|
# sort is required for the following group by operation
|
||||||
|
descriptors = sorted(
|
||||||
|
(package for package_name in packages for package in self.package(package_name)),
|
||||||
|
key=lambda package: package.db.name
|
||||||
|
)
|
||||||
|
|
||||||
|
result: dict[str, set[str]] = {}
|
||||||
|
for database_name, pacman_packages in itertools.groupby(descriptors, lambda package: package.db.name):
|
||||||
|
database_file = self.repository_paths.pacman / "sync" / f"{database_name}.files.tar.gz"
|
||||||
|
if not database_file.is_file():
|
||||||
|
continue # no database file found
|
||||||
|
|
||||||
|
package_names = {package.name: package.version for package in pacman_packages}
|
||||||
|
with tarfile.open(database_file, "r:gz") as archive:
|
||||||
|
result.update(extract(archive, package_names))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def package(self, package_name: str) -> Iterator[Package]:
|
||||||
|
"""
|
||||||
|
retrieve list of the packages from the repository by name
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_name(str): package name to search
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
Package: list of packages which were returned by the query
|
||||||
|
"""
|
||||||
|
for database in self.handle.get_syncdbs():
|
||||||
|
package = database.get_pkg(package_name)
|
||||||
|
if package is None:
|
||||||
|
continue
|
||||||
|
yield package
|
||||||
|
|
||||||
|
def packages(self) -> set[str]:
|
||||||
|
"""
|
||||||
|
get list of packages known for alpm
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
set[str]: list of package names
|
||||||
|
"""
|
||||||
|
result: set[str] = set()
|
||||||
|
for database in self.handle.get_syncdbs():
|
||||||
|
for package in database.pkgcache:
|
||||||
|
# package itself
|
||||||
|
result.add(package.name)
|
||||||
|
# provides list for meta-packages
|
||||||
|
result.update(trim_package(provides) for provides in package.provides)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def provided_by(self, package_name: str) -> Iterator[Package]:
|
||||||
|
"""
|
||||||
|
search through databases and emit packages which provides the ``package_name``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_name(str): package name to search
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
Package: list of packages which were returned by the query
|
||||||
|
"""
|
||||||
|
def is_package_provided(package: Package) -> bool:
|
||||||
|
provides = [trim_package(name) for name in package.provides]
|
||||||
|
return package_name in provides
|
||||||
|
|
||||||
|
for database in self.handle.get_syncdbs():
|
||||||
|
yield from filter(is_package_provided, database.search(package_name))
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from email.utils import parsedate_to_datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from pyalpm import DB # type: ignore[import-not-found]
|
||||||
|
from typing import ClassVar
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.exceptions import PacmanError
|
||||||
|
from ahriman.core.http import SyncHttpClient
|
||||||
|
|
||||||
|
|
||||||
|
class PacmanDatabase(SyncHttpClient):
|
||||||
|
"""
|
||||||
|
implementation for database sync, because pyalpm is not always enough
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
LAST_MODIFIED_HEADER(str): last modified header name
|
||||||
|
database(DB): pyalpm database object
|
||||||
|
repository_paths(RepositoryPaths): repository paths instance
|
||||||
|
sync_files_database(bool): sync files database
|
||||||
|
"""
|
||||||
|
|
||||||
|
LAST_MODIFIED_HEADER: ClassVar[str] = "Last-Modified"
|
||||||
|
|
||||||
|
def __init__(self, database: DB, configuration: Configuration) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
database(DB): pyalpm database object
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
"""
|
||||||
|
SyncHttpClient.__init__(self)
|
||||||
|
self.timeout = None # reset timeout
|
||||||
|
|
||||||
|
self.database = database
|
||||||
|
self.repository_paths = configuration.repository_paths
|
||||||
|
|
||||||
|
self.sync_files_database = configuration.getboolean("alpm", "sync_files_database")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def copy(remote_path: Path, local_path: Path) -> None:
|
||||||
|
"""
|
||||||
|
copy local database file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
remote_path(Path): path to source (remote) file
|
||||||
|
local_path(Path): path to locally stored file
|
||||||
|
"""
|
||||||
|
shutil.copy(remote_path, local_path)
|
||||||
|
|
||||||
|
def download(self, url: str, local_path: Path) -> None:
|
||||||
|
"""
|
||||||
|
download remote file and store it to local path with the correct last modified headers
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url(str): remote url to request file
|
||||||
|
local_path(Path): path to locally stored file
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PacmanError: in case if no last-modified header was found
|
||||||
|
"""
|
||||||
|
response = self.make_request("GET", url, stream=True)
|
||||||
|
if self.LAST_MODIFIED_HEADER not in response.headers:
|
||||||
|
raise PacmanError("No last-modified header found")
|
||||||
|
|
||||||
|
with local_path.open("wb") as local_file:
|
||||||
|
for chunk in response.iter_content(chunk_size=1024):
|
||||||
|
local_file.write(chunk)
|
||||||
|
|
||||||
|
# set correct (a,m)time for the file
|
||||||
|
remote_changed = parsedate_to_datetime(response.headers[self.LAST_MODIFIED_HEADER]).timestamp()
|
||||||
|
os.utime(local_path, (remote_changed, remote_changed))
|
||||||
|
|
||||||
|
def is_outdated(self, url: str, local_path: Path) -> bool:
|
||||||
|
"""
|
||||||
|
check if local file is outdated
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url(str): remote url to request last modified header
|
||||||
|
local_path(Path): path to locally stored file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: ``True`` in case if remote file is newer than local file
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PacmanError: in case if no last-modified header was found
|
||||||
|
"""
|
||||||
|
if not local_path.is_file():
|
||||||
|
return True # no local file found, requires to update
|
||||||
|
|
||||||
|
response = self.make_request("HEAD", url)
|
||||||
|
if self.LAST_MODIFIED_HEADER not in response.headers:
|
||||||
|
raise PacmanError("No last-modified header found")
|
||||||
|
|
||||||
|
remote_changed = parsedate_to_datetime(response.headers["Last-Modified"]).timestamp()
|
||||||
|
local_changed = local_path.stat().st_mtime
|
||||||
|
|
||||||
|
return remote_changed > local_changed
|
||||||
|
|
||||||
|
def sync(self, *, force: bool) -> None:
|
||||||
|
"""
|
||||||
|
sync packages and files databases
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force(bool): force database synchronization (same as ``pacman -Syy``)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.sync_packages(force=force)
|
||||||
|
if self.sync_files_database:
|
||||||
|
self.sync_files(force=force)
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("exception during update %s", self.database.name)
|
||||||
|
|
||||||
|
def sync_files(self, *, force: bool) -> None:
|
||||||
|
"""
|
||||||
|
sync files by using http request
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force(bool): force database synchronization (same as ``pacman -Syy``)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PacmanError: on operation error (invalid scheme or incomplete configuration)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
server = next(iter(self.database.servers))
|
||||||
|
except StopIteration:
|
||||||
|
raise PacmanError("No configured servers available for database") from None
|
||||||
|
filename = f"{self.database.name}.files.tar.gz"
|
||||||
|
url = f"{server}/{filename}"
|
||||||
|
|
||||||
|
remote_uri = urlparse(url)
|
||||||
|
local_path = Path(self.repository_paths.pacman / "sync" / filename)
|
||||||
|
|
||||||
|
match remote_uri.scheme:
|
||||||
|
case "http" | "https":
|
||||||
|
if not force and not self.is_outdated(url, local_path):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.download(url, local_path)
|
||||||
|
|
||||||
|
case "file":
|
||||||
|
# just copy file as it is relatively cheap operation, no need to check timestamps
|
||||||
|
self.copy(Path(remote_uri.path), local_path)
|
||||||
|
|
||||||
|
case other:
|
||||||
|
raise PacmanError(f"Unknown or unsupported URL scheme {other}")
|
||||||
|
|
||||||
|
def sync_packages(self, *, force: bool) -> None:
|
||||||
|
"""
|
||||||
|
sync packages by using built-in pyalpm methods
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force(bool): force database synchronization (same as ``pacman -Syy``)
|
||||||
|
"""
|
||||||
|
self.database.update(force)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from pathlib import Path
|
||||||
|
from pyalpm import Handle, Package # type: ignore[import-not-found]
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from typing import Any, ClassVar, Self
|
||||||
|
|
||||||
|
|
||||||
|
class PacmanHandle:
|
||||||
|
"""
|
||||||
|
lightweight wrapper for pacman handle to be used for direct alpm operations (e.g. package load)
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
handle(Handle): pyalpm handle instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ephemeral: ClassVar[Self | None] = None
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
*args(Any): positional arguments for :class:`pyalpm.Handle`
|
||||||
|
**kwargs(Any): keyword arguments for :class:`pyalpm.Handle`
|
||||||
|
"""
|
||||||
|
self.handle = Handle(*args, **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def ephemeral(cls) -> Self:
|
||||||
|
"""
|
||||||
|
create temporary instance with no access to real databases
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Self: loaded class
|
||||||
|
"""
|
||||||
|
if cls._ephemeral is None:
|
||||||
|
# handle creates alpm version file, but we don't use it
|
||||||
|
# so it is ok to just remove it
|
||||||
|
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
|
||||||
|
cls._ephemeral = cls("/", dir_name)
|
||||||
|
return cls._ephemeral
|
||||||
|
|
||||||
|
def package_load(self, path: Path) -> Package:
|
||||||
|
"""
|
||||||
|
load package from path to the archive
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path(Path): path to package archive
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Package: package instance
|
||||||
|
"""
|
||||||
|
return self.handle.load_pkg(str(path))
|
||||||
|
|
||||||
|
def __getattr__(self, item: str) -> Any:
|
||||||
|
"""
|
||||||
|
proxy methods for :class:`pyalpm.Handle`, because it doesn't allow subclassing
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item(str): property name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: attribute by its name
|
||||||
|
"""
|
||||||
|
return self.handle.__getattribute__(item)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user