Compare commits

..
10 Commits
Author SHA1 Message Date
arcanis 2d66b67123 Release 2.23.3 2026-10-25 02:09:35 +03:00
arcanis 27cfcd07eb fix: handle empty assignment correctly in PKGBUILD 2026-10-25 02:09:13 +03:00
arcanis 2080f1e06f Release 2.23.2 2026-10-25 01:45:45 +03:00
arcanis 2544d44051 fix: refresh databases on repo-update command 2026-10-24 16:48:23 +03:00
arcanis b07ad8cc0c build: clear pacman cache to resolve hashsums mismatch 2026-10-24 12:37:10 +03:00
arcanis c38cb5f680 feat: restore iterable method in parser 2026-10-24 12:20:29 +03:00
arcanis f776b33310 feat: preserve links to packages
This changes url handling in status page. Firstly, instead of using
hash for repository, now they are handled by query parameter. Secondly,
it adds a query parameter for package base. And lastly, it changes
behaviour for clicking on the package link, now it opens status page,
which allows to e.g. open a tab in background
2026-10-22 11:33:06 +03:00
arcanis eafa1b417a build: replace aiohttp-sse-git with aiohttp-sse
The upstream has released new version, which includes all required fixes
for the newest aiohttp
2026-10-20 16:23:56 +03:00
arcanis d0aff05be5 Release 2.23.1 2026-10-20 00:31:19 +03:00
arcanis 8a6b71a425 docs: commit docs requirements.txt 2026-10-20 00:30:57 +03:00
16 changed files with 379 additions and 207 deletions

No files matched your search

+3 -2
View File
@@ -16,7 +16,7 @@ pacman -S --noconfirm --asdeps base-devel npm python-build python-hatchling pyth
# optional dependencies # optional dependencies
if [[ -z $MINIMAL_INSTALL ]]; then if [[ -z $MINIMAL_INSTALL ]]; then
# web server # web server
pacman -S --noconfirm --asdeps 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 pacman -S --noconfirm --asdeps python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-aiohttp-sse python-cryptography python-jinja
# additional features # additional features
pacman -S --noconfirm --asdeps gnupg ipython python-boto3 python-cerberus python-matplotlib python-systemd rsync pacman -S --noconfirm --asdeps gnupg ipython python-boto3 python-cerberus python-matplotlib python-systemd rsync
fi fi
@@ -41,6 +41,7 @@ systemd-machine-id-setup
# remove unused dependencies # remove unused dependencies
pacman -Qdtq | pacman -Rscn --noconfirm - pacman -Qdtq | pacman -Rscn --noconfirm -
find "/var/cache/pacman/pkg" -type f -delete
# initial setup command as root # initial setup command as root
[[ -z $MINIMAL_INSTALL ]] && WEB_ARGS=("--web-port" "8080") [[ -z $MINIMAL_INSTALL ]] && WEB_ARGS=("--web-port" "8080")
@@ -60,6 +61,6 @@ sudo -u ahriman -- ahriman --log-handler console package-add --now ahriman
# check if package was actually installed # check if package was actually installed
test -n "$(find "/var/lib/ahriman/repository/github/x86_64" -name "ahriman*pkg*")" test -n "$(find "/var/lib/ahriman/repository/github/x86_64" -name "ahriman*pkg*")"
# run package check # run package check
sudo -u ahriman -- ahriman repo-update sudo -u ahriman -- ahriman --log-handler console repo-update --refresh
# stop web service lol # stop web service lol
[[ -z $WEB_PID ]] || kill $WEB_PID [[ -z $WEB_PID ]] || kill $WEB_PID
@@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2026\-10\-20" "ahriman 2.23.0" "ArcH linux ReposItory MANager" .TH AHRIMAN "1" "2026\-10\-25" "ahriman 2.23.3" "ArcH linux ReposItory MANager"
.SH NAME .SH NAME
ahriman \- ArcH linux ReposItory MANager ahriman \- ArcH linux ReposItory MANager
.SH SYNOPSIS .SH SYNOPSIS
+1 -1
View File
@@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
__version__ = "2.23.0" __version__ = "2.23.3"
@@ -21,7 +21,7 @@ import re
import shlex import shlex
import tree_sitter_bash import tree_sitter_bash
from collections.abc import Iterator from collections.abc import Iterable, Iterator
from enum import StrEnum from enum import StrEnum
from tree_sitter import Language, Node, Parser from tree_sitter import Language, Node, Parser
from typing import IO, cast from typing import IO, cast
@@ -78,7 +78,7 @@ class PkgbuildNodeField(StrEnum):
Value = "value" Value = "value"
class PkgbuildParser: class PkgbuildParser(Iterable[PkgbuildPatch]):
""" """
PKGBUILD parser implementation in python PKGBUILD parser implementation in python
@@ -97,24 +97,29 @@ class PkgbuildParser:
original source, with their formatting preserved. Assignments inside shell statements and control-flow nodes are original source, with their formatting preserved. Assignments inside shell statements and control-flow nodes are
extracted without checking whether those statements would run. extracted without checking whether those statements would run.
Each iteration parses the stored source independently, starting from the beginning.
Examples: Examples:
Parse package assignments:: Parse package assignments::
>>> source = b"pkgname=example\\npkgver=1\\n" >>> source = b"pkgname=example\\npkgver=1\\n"
>>> for patch in PkgbuildParser(BytesIO(source)).parse(): >>> for patch in PkgbuildParser(BytesIO(source)):
... print(patch.key, patch.value) ... print(patch.key, patch.value)
Function bodies are returned as function patches:: Function bodies are returned as function patches::
>>> source = b"pkgver() { printf 1; }" >>> source = b"pkgver() { printf 1; }"
>>> for patch in list(PkgbuildParser(BytesIO(source)).parse()) >>> for patch in PkgbuildParser(BytesIO(source)):
... print(patch.key, patch.value) ... print(patch.key, patch.value)
""" """
_BRACE_EXPANSION = re.compile(r"(.*)\{([^{}]+)\}(.*)") _BRACE_EXPANSION = re.compile(r"(.*)\{([^{}]+)\}(.*)")
def __init__(self, stream: IO[bytes]) -> None: def __init__(self, stream: IO[bytes]) -> None:
"""""" """
Args:
stream(IO[bytes]): input PKGBUILD stream
"""
self._content = stream.read() self._content = stream.read()
@staticmethod @staticmethod
@@ -164,6 +169,11 @@ class PkgbuildParser:
value = cast(Node, node.child_by_field_name(PkgbuildNodeField.Value)) value = cast(Node, node.child_by_field_name(PkgbuildNodeField.Value))
key = self._text(name) key = self._text(name)
# bash permits assignments without a value (for example ``IFS=``).
# tree-sitter represents those assignments without a value node.
if value is None:
return PkgbuildPatch(key, "")
match value.type: match value.type:
case PkgbuildNodeType.Array: case PkgbuildNodeType.Array:
array_values = [] array_values = []
@@ -256,3 +266,12 @@ class PkgbuildParser:
) )
yield from self._read(tree.root_node) yield from self._read(tree.root_node)
def __iter__(self) -> Iterator[PkgbuildPatch]:
"""
iterate over the parsed entries
Returns:
Iterator[PkgbuildPatch]: parsed entries
"""
return self.parse()
+1 -2
View File
@@ -80,8 +80,7 @@ class Pkgbuild(Mapping[str, Any]):
Returns: Returns:
Self: constructed instance of self Self: constructed instance of self
""" """
parser = PkgbuildParser(stream) fields = {patch.key: patch for patch in PkgbuildParser(stream)}
fields = {patch.key: patch for patch in parser.parse()}
# pkgbase is optional field, the pkgname must be used instead if not set # pkgbase is optional field, the pkgname must be used instead if not set
# however, pkgname is not presented is "package()" functions which we are parsing here too, # however, pkgname is not presented is "package()" functions which we are parsing here too,
@@ -71,7 +71,7 @@ def test_parse_array_exception() -> None:
""" """
parser = PkgbuildParser(BytesIO(b"var=(first second")) parser = PkgbuildParser(BytesIO(b"var=(first second"))
with pytest.raises(PkgbuildParserError): with pytest.raises(PkgbuildParserError):
assert list(parser.parse()) list(parser.parse())
def test_parse_function() -> None: def test_parse_function() -> None:
@@ -119,11 +119,11 @@ def test_parse_function_exception() -> None:
""" """
parser = PkgbuildParser(BytesIO(b"var() echo hello world } ")) parser = PkgbuildParser(BytesIO(b"var() echo hello world } "))
with pytest.raises(PkgbuildParserError): with pytest.raises(PkgbuildParserError):
assert list(parser.parse()) list(parser.parse())
parser = PkgbuildParser(BytesIO(b"var() { echo hello world")) parser = PkgbuildParser(BytesIO(b"var() { echo hello world"))
with pytest.raises(PkgbuildParserError): with pytest.raises(PkgbuildParserError):
assert list(parser.parse()) list(parser.parse())
def test_parse_function_inner_shell() -> None: def test_parse_function_inner_shell() -> None:
@@ -578,6 +578,7 @@ def test_walk(resource_path_root: Path) -> None:
resource_path_root / "models" / "package_pacman-static_pkgbuild", resource_path_root / "models" / "package_pacman-static_pkgbuild",
resource_path_root / "models" / "package_python-pytest-loop_pkgbuild", resource_path_root / "models" / "package_python-pytest-loop_pkgbuild",
resource_path_root / "models" / "package_tpacpi-bat-git_pkgbuild", resource_path_root / "models" / "package_tpacpi-bat-git_pkgbuild",
resource_path_root / "models" / "package_ttf-google-fonts-git_pkgbuild",
resource_path_root / "models" / "package_vim-youcompleteme-git_pkgbuild", resource_path_root / "models" / "package_vim-youcompleteme-git_pkgbuild",
resource_path_root / "models" / "package_yay_pkgbuild", resource_path_root / "models" / "package_yay_pkgbuild",
resource_path_root / "models" / "pkgbuild", resource_path_root / "models" / "pkgbuild",
+250 -168
View File
@@ -50,7 +50,7 @@ def test_from_io(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None:
must correctly load from io must correctly load from io
""" """
load_mock = mocker.patch("ahriman.core.alpm.pkgbuild_parser.PkgbuildParser.parse", load_mock = mocker.patch("ahriman.core.alpm.pkgbuild_parser.PkgbuildParser.parse",
return_value=pkgbuild_ahriman.fields.values()) return_value=iter(pkgbuild_ahriman.fields.values()))
assert Pkgbuild.from_io(BytesIO(b"mock")) == pkgbuild_ahriman assert Pkgbuild.from_io(BytesIO(b"mock")) == pkgbuild_ahriman
load_mock.assert_called_once_with() load_mock.assert_called_once_with()
@@ -60,9 +60,9 @@ def test_from_io_pkgbase(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> N
must assign missing pkgbase if pkgname is presented must assign missing pkgbase if pkgname is presented
""" """
mocker.patch("ahriman.core.alpm.pkgbuild_parser.PkgbuildParser.parse", side_effect=[ mocker.patch("ahriman.core.alpm.pkgbuild_parser.PkgbuildParser.parse", side_effect=[
[value for key, value in pkgbuild_ahriman.fields.items() if key not in ("pkgbase",)], iter([value for key, value in pkgbuild_ahriman.fields.items() if key not in ("pkgbase",)]),
[value for key, value in pkgbuild_ahriman.fields.items() if key not in ("pkgbase", "pkgname",)], iter([value for key, value in pkgbuild_ahriman.fields.items() if key not in ("pkgbase", "pkgname",)]),
[value for key, value in pkgbuild_ahriman.fields.items()] + [PkgbuildPatch("pkgbase", "pkgbase")], iter([value for key, value in pkgbuild_ahriman.fields.items()] + [PkgbuildPatch("pkgbase", "pkgbase")]),
]) ])
assert Pkgbuild.from_io(BytesIO(b"mock"))["pkgbase"] == pkgbuild_ahriman["pkgname"] assert Pkgbuild.from_io(BytesIO(b"mock"))["pkgbase"] == pkgbuild_ahriman["pkgname"]
@@ -75,7 +75,7 @@ def test_from_io_empty(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> Non
must skip empty patches must skip empty patches
""" """
mocker.patch("ahriman.core.alpm.pkgbuild_parser.PkgbuildParser.parse", mocker.patch("ahriman.core.alpm.pkgbuild_parser.PkgbuildParser.parse",
return_value=list(pkgbuild_ahriman.fields.values()) + [PkgbuildPatch("", "")]) return_value=iter(list(pkgbuild_ahriman.fields.values()) + [PkgbuildPatch("", "")]))
assert Pkgbuild.from_io(BytesIO(b"mock")) == pkgbuild_ahriman assert Pkgbuild.from_io(BytesIO(b"mock")) == pkgbuild_ahriman
@@ -399,169 +399,6 @@ def test_parse_jellyfin_ffmpeg6_bin(resource_path_root: Path) -> None:
} }
def test_parse_tpacpi_bat_git(resource_path_root: Path) -> None:
"""
must parse real PKGBUILDs correctly (tpacpi-bat-git)
"""
pkgbuild = Pkgbuild.from_file(resource_path_root / "models" / "package_tpacpi-bat-git_pkgbuild")
values = {key: value.value for key, value in pkgbuild.fields.items() if not value.is_function}
assert values == {
"pkgbase": "tpacpi-bat-git",
"pkgname": "tpacpi-bat-git",
"pkgver": "3.1.r13.g4959b52",
"pkgrel": "1",
"pkgdesc": "A Perl script with ACPI calls for recent ThinkPads which are not supported by tp_smapi",
"arch": ["any"],
"url": "https://github.com/teleshoes/tpacpi-bat",
"license": ["GPL3"],
"depends": ["perl", "acpi_call"],
"makedepends": ["git"],
"provides": ["tpacpi-bat"],
"conflicts": ["tpacpi-bat"],
"backup": ["etc/conf.d/tpacpi"],
"source": ["git+https://github.com/teleshoes/tpacpi-bat.git"],
"b2sums": ["SKIP"],
}
def test_parse_yay(resource_path_root: Path) -> None:
"""
must parse real PKGBUILDs correctly (yay)
"""
pkgbuild = Pkgbuild.from_file(resource_path_root / "models" / "package_yay_pkgbuild")
values = {key: value.value for key, value in pkgbuild.fields.items() if not value.is_function}
assert values == {
"pkgbase": "yay",
"pkgname": "yay",
"pkgver": "12.3.5",
"pkgrel": "1",
"pkgdesc": "Yet another yogurt. Pacman wrapper and AUR helper written in go.",
"arch": [
"i686",
"pentium4",
"x86_64",
"arm",
"armv7h",
"armv6h",
"aarch64",
"riscv64",
],
"url": "https://github.com/Jguer/yay",
"options": ["!lto"],
"license": ["GPL-3.0-or-later"],
"depends": [
"pacman>6.1",
"git",
],
"optdepends": [
"sudo: privilege elevation",
"doas: privilege elevation",
],
"makedepends": ["go>=1.21"],
"source": ["${pkgname}-${pkgver}.tar.gz::https://github.com/Jguer/yay/archive/v${pkgver}.tar.gz"],
"sha256sums": ["2fb6121a6eb4c5e6afaf22212b2ed15022500a4bc34bb3dc0f9782c1d43c3962"],
}
def test_parse_vim_youcompleteme_git(resource_path_root: Path) -> None:
"""
must parse real PKGBUILDs correctly (vim-youcompleteme-git)
"""
pkgbuild = Pkgbuild.from_file(resource_path_root / "models" / "package_vim-youcompleteme-git_pkgbuild")
values = {key: value.value for key, value in pkgbuild.fields.items() if not value.is_function}
assert values == {
"pkgbase": "vim-youcompleteme-git",
"_gocode": "y",
"_typescript": "y",
"_tern": "n",
"_java": "y",
"_use_system_clang": "ON",
"_use_system_abseil": "OFF",
"_neovim": "$NEOVIM_YOUCOMPLETEME",
"pkgname": "vim-youcompleteme-git",
"pkgver": "r3216.0d855962",
"pkgrel": "1",
"pkgdesc": "A code-completion engine for Vim",
"arch": ["x86_64"],
"url": "https://ycm-core.github.io/YouCompleteMe/",
"license": ["GPL3"],
"groups": ["vim-plugins"],
"depends": [
"vim",
"python>=3.6",
"python-watchdog",
"python-bottle",
"clang"
],
"makedepends": [
"git",
"cmake",
"pybind11",
],
"optdepends": [
"gopls: Go semantic completion",
"nodejs-tern: JavaScript semantic completion",
"rust-analyzer: Rust semantic completion",
"typescript: Typescript semantic completion",
"python-jedi: Python semantic completion",
"python-numpydoc: Python semantic completion",
"python-regex: Better Unicode support",
"omnisharp-roslyn: C# semantic completion",
"java-environment>=11: Java semantic completion",
"jdtls: Java semantic completion",
"abseil-cpp: if setting _use_system_abseil ON",
],
"source": [
"git+https://github.com/ycm-core/YouCompleteMe.git",
"git+https://github.com/ycm-core/ycmd.git",
"clangd-15.0.1.tar.bz2::https://github.com/ycm-core/llvm/releases/download/15.0.1/clangd-15.0.1-x86_64-unknown-linux-gnu.tar.bz2",
"libclang-15.0.1.tar.bz2::https://github.com/ycm-core/llvm/releases/download/15.0.1/libclang-15.0.1-x86_64-unknown-linux-gnu.tar.bz2",
],
"sha256sums": [
"SKIP",
"SKIP",
"10a64c468d1dd2a384e0e5fd4eb2582fd9f1dfa706b6d2d2bb88fb0fbfc2718d",
"9a5bee818a4995bc52e91588059bef42728d046808206bfb93977f4e3109e50c",
],
}
def test_parse_python_pytest_loop(resource_path_root: Path) -> None:
"""
must parse real PKGBUILDs correctly (python-pytest-loop)
"""
pkgbuild = Pkgbuild.from_file(resource_path_root / "models" / "package_python-pytest-loop_pkgbuild")
values = {key: value.value for key, value in pkgbuild.fields.items() if not value.is_function}
assert values == {
"pkgbase": "python-pytest-loop",
"_pname": "${pkgbase#python-}",
"_pyname": "${_pname//-/_}",
"pkgname": ["python-${_pname}"],
"pkgver": "1.0.13",
"pkgrel": "1",
"pkgdesc": "Pytest plugin for looping test execution.",
"arch": ["any"],
"url": "https://github.com/anogowski/pytest-loop",
"license": ["MPL-2.0"],
"makedepends": [
"python-hatchling",
"python-versioningit",
"python-wheel",
"python-build",
"python-installer",
],
"checkdepends": [
"python-pytest",
],
"source": [
"https://files.pythonhosted.org/packages/source/${_pyname:0:1}/${_pyname}/${_pyname}-${pkgver}.tar.gz",
],
"md5sums": [
"98365f49606d5068f92350f1d2569a5f",
],
}
def test_parse_pacman_static(resource_path_root: Path) -> None: def test_parse_pacman_static(resource_path_root: Path) -> None:
""" """
must parse real PKGBUILDs correctly (pacman-static) must parse real PKGBUILDs correctly (pacman-static)
@@ -654,3 +491,248 @@ def test_parse_pacman_static(resource_path_root: Path) -> None:
"CXXFLAGS": "${CXXFLAGS/-fstack-protector-strong/}", "CXXFLAGS": "${CXXFLAGS/-fstack-protector-strong/}",
"PKGEXT": ".pkg.tar.xz", "PKGEXT": ".pkg.tar.xz",
} }
def test_parse_python_pytest_loop(resource_path_root: Path) -> None:
"""
must parse real PKGBUILDs correctly (python-pytest-loop)
"""
pkgbuild = Pkgbuild.from_file(resource_path_root / "models" / "package_python-pytest-loop_pkgbuild")
values = {key: value.value for key, value in pkgbuild.fields.items() if not value.is_function}
assert values == {
"pkgbase": "python-pytest-loop",
"_pname": "${pkgbase#python-}",
"_pyname": "${_pname//-/_}",
"pkgname": ["python-${_pname}"],
"pkgver": "1.0.13",
"pkgrel": "1",
"pkgdesc": "Pytest plugin for looping test execution.",
"arch": ["any"],
"url": "https://github.com/anogowski/pytest-loop",
"license": ["MPL-2.0"],
"makedepends": [
"python-hatchling",
"python-versioningit",
"python-wheel",
"python-build",
"python-installer",
],
"checkdepends": [
"python-pytest",
],
"source": [
"https://files.pythonhosted.org/packages/source/${_pyname:0:1}/${_pyname}/${_pyname}-${pkgver}.tar.gz",
],
"md5sums": [
"98365f49606d5068f92350f1d2569a5f",
],
}
def test_parse_tpacpi_bat_git(resource_path_root: Path) -> None:
"""
must parse real PKGBUILDs correctly (tpacpi-bat-git)
"""
pkgbuild = Pkgbuild.from_file(resource_path_root / "models" / "package_tpacpi-bat-git_pkgbuild")
values = {key: value.value for key, value in pkgbuild.fields.items() if not value.is_function}
assert values == {
"pkgbase": "tpacpi-bat-git",
"pkgname": "tpacpi-bat-git",
"pkgver": "3.1.r13.g4959b52",
"pkgrel": "1",
"pkgdesc": "A Perl script with ACPI calls for recent ThinkPads which are not supported by tp_smapi",
"arch": ["any"],
"url": "https://github.com/teleshoes/tpacpi-bat",
"license": ["GPL3"],
"depends": ["perl", "acpi_call"],
"makedepends": ["git"],
"provides": ["tpacpi-bat"],
"conflicts": ["tpacpi-bat"],
"backup": ["etc/conf.d/tpacpi"],
"source": ["git+https://github.com/teleshoes/tpacpi-bat.git"],
"b2sums": ["SKIP"],
}
def test_parse_ttf_google_fonts_git(resource_path_root: Path) -> None:
"""
must parse real PKGBUILDs correctly (ttf-google-fonts-git)
"""
pkgbuild = Pkgbuild.from_file(resource_path_root / "models" / "package_ttf-google-fonts-git_pkgbuild")
values = {key: value.value for key, value in pkgbuild.fields.items() if not value.is_function}
assert "ttf-google-fonts-git" in pkgbuild.packages()
assert values == {
"pkgname": "ttf-google-fonts-git",
"pkgver": "r11002.cdabad3d0",
"pkgrel": "1",
"epoch": "1",
"pkgdesc": "TrueType fonts from the Google Fonts project (git version)",
"arch": ["any"],
"url": "https://github.com/google/fonts",
"license": ["custom:SIL Open Font License", "custom:Ubuntu Font License v1.0"],
"depends": [
"adobe-source-code-pro-fonts",
"adobe-source-sans-fonts",
"adobe-source-serif-fonts",
"cantarell-fonts",
"noto-fonts",
"noto-fonts-extra",
"ttf-anonymous-pro",
"ttf-caladea",
"ttf-croscore",
"ttf-fira-sans",
"ttf-fira-mono",
"ttf-inconsolata",
"ttf-lato",
"ttf-merriweather",
"ttf-merriweather-sans",
"ttf-opensans",
"ttf-oswald",
"ttf-quintessential",
"ttf-roboto",
"ttf-roboto-mono",
"ttf-signika",
"ttf-ubuntu-font-family",
],
"makedepends": ["git", "fontconfig"],
"conflicts": [
"jsmath-fonts",
"lohit-fonts",
"ttf-andika",
"ttf-arabeyes-fonts",
"ttf-cardo",
"ttf-comfortaa",
"ttf-google-fonts-typewolf",
"ttf-lora-cyrillic",
"ttf-lekton",
"ttf-medievalsharp",
"ttf-nova",
"ttf-oxygen",
"ttf-oxygen-git",
"ttf-pt-fonts",
"ttf-source-code-pro-ibx",
"ttf-source-sans-pro-ibx",
"ttf-vollkorn-ibx",
],
"provides": [
"jsmath-fonts",
"lohit-fonts",
"ttf-andika",
"ttf-cardo",
"ttf-carlito",
"ttf-comfortaa",
"ttf-lora-cyrillic",
"ttf-lekton",
"ttf-medievalsharp",
"ttf-nova",
"ttf-oxygen",
"ttf-oxygen-git",
"ttf-pt-fonts",
"ttf-source-code-pro-ibx",
"ttf-source-sans-pro-ibx",
"ttf-vollkorn-ibx",
],
"source": ["git+${url}.git"],
"sha512sums": ["SKIP"],
"pkgbase": "ttf-google-fonts-git",
}
def test_parse_vim_youcompleteme_git(resource_path_root: Path) -> None:
"""
must parse real PKGBUILDs correctly (vim-youcompleteme-git)
"""
pkgbuild = Pkgbuild.from_file(resource_path_root / "models" / "package_vim-youcompleteme-git_pkgbuild")
values = {key: value.value for key, value in pkgbuild.fields.items() if not value.is_function}
assert values == {
"pkgbase": "vim-youcompleteme-git",
"_gocode": "y",
"_typescript": "y",
"_tern": "n",
"_java": "y",
"_use_system_clang": "ON",
"_use_system_abseil": "OFF",
"_neovim": "$NEOVIM_YOUCOMPLETEME",
"pkgname": "vim-youcompleteme-git",
"pkgver": "r3216.0d855962",
"pkgrel": "1",
"pkgdesc": "A code-completion engine for Vim",
"arch": ["x86_64"],
"url": "https://ycm-core.github.io/YouCompleteMe/",
"license": ["GPL3"],
"groups": ["vim-plugins"],
"depends": [
"vim",
"python>=3.6",
"python-watchdog",
"python-bottle",
"clang"
],
"makedepends": [
"git",
"cmake",
"pybind11",
],
"optdepends": [
"gopls: Go semantic completion",
"nodejs-tern: JavaScript semantic completion",
"rust-analyzer: Rust semantic completion",
"typescript: Typescript semantic completion",
"python-jedi: Python semantic completion",
"python-numpydoc: Python semantic completion",
"python-regex: Better Unicode support",
"omnisharp-roslyn: C# semantic completion",
"java-environment>=11: Java semantic completion",
"jdtls: Java semantic completion",
"abseil-cpp: if setting _use_system_abseil ON",
],
"source": [
"git+https://github.com/ycm-core/YouCompleteMe.git",
"git+https://github.com/ycm-core/ycmd.git",
"clangd-15.0.1.tar.bz2::https://github.com/ycm-core/llvm/releases/download/15.0.1/clangd-15.0.1-x86_64-unknown-linux-gnu.tar.bz2",
"libclang-15.0.1.tar.bz2::https://github.com/ycm-core/llvm/releases/download/15.0.1/libclang-15.0.1-x86_64-unknown-linux-gnu.tar.bz2",
],
"sha256sums": [
"SKIP",
"SKIP",
"10a64c468d1dd2a384e0e5fd4eb2582fd9f1dfa706b6d2d2bb88fb0fbfc2718d",
"9a5bee818a4995bc52e91588059bef42728d046808206bfb93977f4e3109e50c",
],
}
def test_parse_yay(resource_path_root: Path) -> None:
"""
must parse real PKGBUILDs correctly (yay)
"""
pkgbuild = Pkgbuild.from_file(resource_path_root / "models" / "package_yay_pkgbuild")
values = {key: value.value for key, value in pkgbuild.fields.items() if not value.is_function}
assert values == {
"pkgbase": "yay",
"pkgname": "yay",
"pkgver": "12.3.5",
"pkgrel": "1",
"pkgdesc": "Yet another yogurt. Pacman wrapper and AUR helper written in go.",
"arch": [
"i686",
"pentium4",
"x86_64",
"arm",
"armv7h",
"armv6h",
"aarch64",
"riscv64",
],
"url": "https://github.com/Jguer/yay",
"options": ["!lto"],
"license": ["GPL-3.0-or-later"],
"depends": [
"pacman>6.1",
"git",
],
"optdepends": [
"sudo: privilege elevation",
"doas: privilege elevation",
],
"makedepends": ["go>=1.21"],
"source": ["${pkgname}-${pkgver}.tar.gz::https://github.com/Jguer/yay/archive/v${pkgver}.tar.gz"],
"sha256sums": ["2fb6121a6eb4c5e6afaf22212b2ed15022500a4bc34bb3dc0f9782c1d43c3962"],
}
+2 -2
View File
@@ -2,7 +2,7 @@
pkgbase='ahriman' pkgbase='ahriman'
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web') pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
pkgver=2.23.0 pkgver=2.23.3
pkgrel=1 pkgrel=1
pkgdesc="ArcH linux ReposItory MANager" pkgdesc="ArcH linux ReposItory MANager"
arch=('any') arch=('any')
@@ -80,7 +80,7 @@ package_ahriman-triggers() {
package_ahriman-web() { package_ahriman-web() {
pkgname='ahriman-web' pkgname='ahriman-web'
pkgdesc="ArcH linux ReposItory MANager, web server" pkgdesc="ArcH linux ReposItory MANager, web server"
depends=("$pkgbase-core=$pkgver" 'python-aiohttp-cors' 'python-aiohttp-jinja2' 'python-aiohttp-sse-git') depends=("$pkgbase-core=$pkgver" 'python-aiohttp-cors' 'python-aiohttp-jinja2' 'python-aiohttp-sse')
optdepends=('python-aioauth-client: OAuth2 authorization support' optdepends=('python-aioauth-client: OAuth2 authorization support'
'python-aiohttp-apispec>=3.0.0: autogenerated API documentation' 'python-aiohttp-apispec>=3.0.0: autogenerated API documentation'
'python-aiohttp-openmetrics: HTTP metrics support' 'python-aiohttp-openmetrics: HTTP metrics support'
+1 -1
View File
@@ -59,7 +59,7 @@ RUN runuser -u build -- install-aur-package \
python-aiohttp-jinja2 \ python-aiohttp-jinja2 \
python-aiohttp-session \ python-aiohttp-session \
python-aiohttp-security \ python-aiohttp-security \
python-aiohttp-sse-git \ python-aiohttp-sse \
python-requests-unixsocket2 \ python-requests-unixsocket2 \
python-tree-sitter \ python-tree-sitter \
python-tree-sitter-bash python-tree-sitter-bash
+4
View File
@@ -125,6 +125,10 @@ sphinxcontrib-serializinghtml==2.0.0
# via sphinx # via sphinx
stdlib-list==0.11.1 stdlib-list==0.11.1
# via pydeps # via pydeps
tree-sitter==0.26.0
# via ahriman-core
tree-sitter-bash==0.25.1
# via ahriman-core
urllib3==2.4.0 urllib3==2.4.0
# via # via
# botocore # botocore
+1 -1
View File
@@ -42,5 +42,5 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"type": "module", "type": "module",
"version": "2.23.0" "version": "2.23.3"
} }
+19 -6
View File
@@ -37,7 +37,9 @@ import PackageRebuildDialog from "components/dialogs/PackageRebuildDialog";
import PackageTableToolbar from "components/table/PackageTableToolbar"; import PackageTableToolbar from "components/table/PackageTableToolbar";
import StatusCell from "components/table/StatusCell"; import StatusCell from "components/table/StatusCell";
import { useDebounce } from "hooks/useDebounce"; import { useDebounce } from "hooks/useDebounce";
import { navigate, packageHref } from "hooks/useLocationQuery";
import { usePackageTable } from "hooks/usePackageTable"; import { usePackageTable } from "hooks/usePackageTable";
import { useRepository } from "hooks/useRepository";
import type { PackageRow } from "models/PackageRow"; import type { PackageRow } from "models/PackageRow";
import React, { useMemo } from "react"; import React, { useMemo } from "react";
@@ -59,6 +61,7 @@ function createListColumn(
export default function PackageTable(): React.JSX.Element { export default function PackageTable(): React.JSX.Element {
const table = usePackageTable(); const table = usePackageTable();
const { currentRepository } = useRepository();
const { handleRemove, handleUpdate, isAuthorized } = table; const { handleRemove, handleUpdate, isAuthorized } = table;
const apiRef = useGridApiRef(); const apiRef = useGridApiRef();
const debouncedSearch = useDebounce(table.searchText, 300); const debouncedSearch = useDebounce(table.searchText, 300);
@@ -79,11 +82,20 @@ export default function PackageTable(): React.JSX.Element {
headerName: "package base", headerName: "package base",
minWidth: 150, minWidth: 150,
renderCell: params => renderCell: params =>
params.row.webUrl ? <Link
<Link href={params.row.webUrl} rel="noopener noreferrer" target="_blank" underline="hover"> href={packageHref(currentRepository?.key ?? "", params.row.base)}
{params.value as string} onClick={event => {
</Link> const modified = event.ctrlKey || event.metaKey || event.shiftKey || event.altKey;
: params.value as string, if (event.button === 0 && !modified) {
event.preventDefault();
navigate(packageHref(currentRepository?.key ?? "", params.row.base));
}
}}
tabIndex={params.tabIndex}
underline="hover"
>
{params.value as string}
</Link>,
}, },
{ align: "right", field: "version", headerAlign: "right", headerName: "version", width: 180 }, { align: "right", field: "version", headerAlign: "right", headerName: "version", width: 180 },
createListColumn("packages", "packages", { flex: 1, minWidth: 120 }), createListColumn("packages", "packages", { flex: 1, minWidth: 120 }),
@@ -124,7 +136,7 @@ export default function PackageTable(): React.JSX.Element {
], ],
} satisfies GridColDef<PackageRow>] : [], } satisfies GridColDef<PackageRow>] : [],
], ],
[handleRemove, handleUpdate, isAuthorized], [currentRepository, handleRemove, handleUpdate, isAuthorized],
); );
return <Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}> return <Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
@@ -195,6 +207,7 @@ export default function PackageTable(): React.JSX.Element {
<PackageRebuildDialog onClose={() => table.setDialogOpen(null)} open={table.dialogOpen === "rebuild"} /> <PackageRebuildDialog onClose={() => table.setDialogOpen(null)} open={table.dialogOpen === "rebuild"} />
<KeyImportDialog onClose={() => table.setDialogOpen(null)} open={table.dialogOpen === "keyImport"} /> <KeyImportDialog onClose={() => table.setDialogOpen(null)} open={table.dialogOpen === "keyImport"} />
<PackageInfoDialog <PackageInfoDialog
key={`${currentRepository?.key}/${table.selectedPackage}`}
onClose={() => table.setSelectedPackage(null)} onClose={() => table.setSelectedPackage(null)}
open={table.selectedPackage !== null} open={table.selectedPackage !== null}
packageBase={table.selectedPackage} packageBase={table.selectedPackage}
+6 -14
View File
@@ -18,31 +18,23 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { RepositoryContext } from "contexts/RepositoryContext"; import { RepositoryContext } from "contexts/RepositoryContext";
import { navigate, packageHref, useLocationQuery } from "hooks/useLocationQuery";
import type { RepositoryId } from "models/RepositoryId"; import type { RepositoryId } from "models/RepositoryId";
import React, { type ReactNode, useCallback, useMemo, useState, useSyncExternalStore } from "react"; import React, { type ReactNode, useCallback, useMemo, useState } from "react";
function subscribeToHash(callback: () => void): () => void {
window.addEventListener("hashchange", callback);
return () => window.removeEventListener("hashchange", callback);
}
function getHashSnapshot(): string {
return window.location.hash.replace("#", "");
}
export function RepositoryProvider({ children }: { children: ReactNode }): React.JSX.Element { export function RepositoryProvider({ children }: { children: ReactNode }): React.JSX.Element {
const [repositories, setRepositories] = useState<RepositoryId[]>([]); const [repositories, setRepositories] = useState<RepositoryId[]>([]);
const hash = useSyncExternalStore(subscribeToHash, getHashSnapshot); const repositoryKey = new URLSearchParams(useLocationQuery()).get("repository_id");
const currentRepository = useMemo(() => { const currentRepository = useMemo(() => {
if (repositories.length === 0) { if (repositories.length === 0) {
return null; return null;
} }
return repositories.find(repository => repository.key === hash) ?? repositories[0] ?? null; return repositories.find(repository => repository.key === repositoryKey) ?? repositories[0] ?? null;
}, [repositories, hash]); }, [repositories, repositoryKey]);
const setCurrentRepository = useCallback((repository: RepositoryId) => { const setCurrentRepository = useCallback((repository: RepositoryId) => {
window.location.hash = repository.key; navigate(packageHref(repository.key, null));
}, []); }, []);
const value = useMemo(() => ({ const value = useMemo(() => ({
+52
View File
@@ -0,0 +1,52 @@
/*
* 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 { useSyncExternalStore } from "react";
function subscribe(callback: () => void): () => void {
window.addEventListener("popstate", callback);
return () => window.removeEventListener("popstate", callback);
}
function getSnapshot(): string {
return window.location.search;
}
export function useLocationQuery(): string {
return useSyncExternalStore(subscribe, getSnapshot);
}
export function packageHref(repositoryKey: string, packageBase: string | null): string {
const query = new URLSearchParams(window.location.search);
query.set("repository_id", repositoryKey);
if (packageBase) {
query.set("package", packageBase);
} else {
query.delete("package");
}
return `${window.location.pathname}?${query}`;
}
export function navigate(href: string): void {
if (href === `${window.location.pathname}${window.location.search}`) {
return;
}
window.history.pushState(null, "", href);
window.dispatchEvent(new PopStateEvent("popstate"));
}
+10 -1
View File
@@ -19,6 +19,8 @@
*/ */
import type { GridFilterModel } from "@mui/x-data-grid"; import type { GridFilterModel } from "@mui/x-data-grid";
import { useLocalStorage } from "hooks/useLocalStorage"; import { useLocalStorage } from "hooks/useLocalStorage";
import { navigate, packageHref, useLocationQuery } from "hooks/useLocationQuery";
import { useRepository } from "hooks/useRepository";
import { useState } from "react"; import { useState } from "react";
export type DialogType = "dashboard" | "add" | "rebuild" | "keyImport"; export type DialogType = "dashboard" | "add" | "rebuild" | "keyImport";
@@ -43,7 +45,14 @@ export interface UseTableStateResult {
export function useTableState(): UseTableStateResult { export function useTableState(): UseTableStateResult {
const [selectionModel, setSelectionModel] = useState<string[]>([]); const [selectionModel, setSelectionModel] = useState<string[]>([]);
const [dialogOpen, setDialogOpen] = useState<DialogType | null>(null); const [dialogOpen, setDialogOpen] = useState<DialogType | null>(null);
const [selectedPackage, setSelectedPackage] = useState<string | null>(null); const { currentRepository } = useRepository();
const packageBase = new URLSearchParams(useLocationQuery()).get("package");
const selectedPackage = currentRepository && packageBase ? packageBase : null;
const setSelectedPackage = (base: string | null): void => {
if (currentRepository) {
navigate(packageHref(currentRepository.key, base));
}
};
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [paginationModel, setPaginationModel] = useLocalStorage("ahriman-packages-pagination", { const [paginationModel, setPaginationModel] = useLocalStorage("ahriman-packages-pagination", {