Compare commits

..

4 Commits

Author SHA1 Message Date
6bfa0d26d4 Release 2.15.2 2024-09-26 16:58:19 +03:00
343435b3bf fix: fix pkgbuild parsing in some cases
It has been found that there are two cases in which pkgbuild was not
parsed correctly

1. Major case in which there is quotation mark inside comment line,
   which would cause ValueError: No closing quotation error
2. Minor case, if there are utf symbols in pkgbuild file (e.g.
   hieroglyphs, see ttf-google-fonts-git), it will case incorrect
   reading in `_is_escaped` method
2024-09-26 16:48:38 +03:00
f0930be238 fix: do not copy own database during pyalpm initialization
Previous implementation lead to warning in logs in case if the
repository itself wasn't configured on the host
2024-09-25 14:31:32 +03:00
113a861f31 fix: suppress info logging during version check 2024-09-25 14:23:53 +03:00
24 changed files with 6543 additions and 6628 deletions

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=2.15.1
pkgver=2.15.2
pkgrel=1
pkgdesc="ArcH linux ReposItory MANager"
arch=('any')

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2024\-09\-24" "ahriman" "Generated Python Manual"
.TH AHRIMAN "1" "2024\-09\-26" "ahriman" "Generated Python Manual"
.SH NAME
ahriman
.SH SYNOPSIS
@ -989,7 +989,25 @@ usage: ahriman web [\-h]
start web server
.SH COMMENTS
Argument list can also be read from file by using @ prefix.
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
.SH AUTHOR
.nf

View File

@ -17,4 +17,4 @@
# 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.15.1"
__version__ = "2.15.2"

View File

@ -141,7 +141,7 @@ class Setup(Handler):
(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") as ahriman_configuration:
with target.open("w", encoding="utf8") as ahriman_configuration:
configuration.write(ahriman_configuration)
@staticmethod
@ -191,7 +191,7 @@ class Setup(Handler):
configuration.set_option(repository_id.name, "Server", repository_server)
target = source.parent / f"{repository_id.name}-{repository_id.architecture}.conf"
with target.open("w") as devtools_configuration:
with target.open("w", encoding="utf8") as devtools_configuration:
configuration.write(devtools_configuration)
@staticmethod

View File

@ -112,7 +112,7 @@ class Lock(LazyLogging):
"""
if self.path is None:
return
self._pid_file = self.path.open("a+")
self._pid_file = self.path.open("a+", encoding="utf8")
def _watch(self) -> bool:
"""

View File

@ -91,9 +91,8 @@ class Pacman(LazyLogging):
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
local_database = self.database_init(handle, self.repository_id.name, self.repository_id.architecture)
self.database_copy(handle, local_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)
@ -115,6 +114,7 @@ class Pacman(LazyLogging):
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():
@ -123,11 +123,13 @@ class Pacman(LazyLogging):
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 from operating system root to ahriman's home")
self.logger.info("copy pacman database %s from operating system root to ahriman's home %s", src, dst)
shutil.copy(src, dst)
self.repository_paths.chown(dst)

View File

@ -174,18 +174,31 @@ class PkgbuildParser(shlex.shlex):
Returns:
bool: ``True`` if the previous element of the stream is a quote or escaped and ``False`` otherwise
"""
# wrapper around reading utf symbols from random position of the stream
def read_last() -> tuple[int, str]:
while (position := self._io.tell()) > 0:
try:
return position, self._io.read(1)
except UnicodeDecodeError:
self._io.seek(position - 1)
raise PkgbuildParserError("reached starting position, no valid symbols found")
current_position = self._io.tell()
last_char = penultimate_char = None
for index in range(current_position - 1, -1, -1):
index = current_position - 1
while index > 0:
self._io.seek(index)
last_char = self._io.read(1)
index, last_char = read_last()
if last_char.isspace():
index -= 1
continue
if index >= 0:
if index > 1:
self._io.seek(index - 1)
penultimate_char = self._io.read(1)
_, penultimate_char = read_last()
break
@ -216,6 +229,7 @@ class PkgbuildParser(shlex.shlex):
case PkgbuildToken.Comment:
self.instream.readline()
continue
yield token
if token != PkgbuildToken.ArrayEnds:
@ -248,24 +262,28 @@ class PkgbuildParser(shlex.shlex):
counter += 1
case PkgbuildToken.FunctionEnds:
end_position = self._io.tell()
if self.state != self.eof: # type: ignore[attr-defined]
end_position -= 1 # if we are not at the end of the file, position is _after_ the token
counter -= 1
if counter == 0:
break
case PkgbuildToken.Comment:
self.instream.readline()
if not 0 < start_position < end_position:
raise PkgbuildParserError("function body wasn't found")
# read the specified interval from source stream
self._io.seek(start_position - 1) # start from the previous symbol
content = self._io.read(end_position - start_position)
# we cannot use :func:`read()` here, because it reads characters, not bytes
content = ""
while self._io.tell() != end_position and (next_char := self._io.read(1)):
content += next_char
# special case of the end of file
if self.state == self.eof: # type: ignore[attr-defined]
content += self._io.read(1)
# reset position (because the last position was before the next token starts)
self._io.seek(end_position)
return content
def _parse_token(self, token: str) -> Generator[PkgbuildPatch, None, None]:

View File

@ -141,7 +141,7 @@ def migrate_package_statuses(connection: Connection, paths: RepositoryPaths) ->
cache_path = paths.root / "status_cache.json"
if not cache_path.is_file():
return # no file found
with cache_path.open() as cache:
with cache_path.open(encoding="utf8") as cache:
dump = json.load(cache)
for item in dump.get("packages", []):

View File

@ -23,9 +23,8 @@ from collections.abc import Callable
from pathlib import Path
from typing import Any, TypeVar
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
T = TypeVar("T")
@ -39,16 +38,16 @@ class Operations(LazyLogging):
path(Path): path to the database file
"""
def __init__(self, path: Path, repository_id: RepositoryId, repository_paths: RepositoryPaths) -> None:
def __init__(self, path: Path, configuration: Configuration) -> None:
"""
Args:
path(Path): path to the database file
repository_id(RepositoryId): repository unique identifier
repository_paths(RepositoryPaths): repository paths
configuration(Configuration): configuration instance
"""
self.path = path
self._repository_id = repository_id
self._repository_paths = repository_paths
self._configuration = configuration
_, self._repository_id = configuration.check_loaded()
self._repository_paths = configuration.repository_paths
@property
def logger_name(self) -> str:

View File

@ -66,10 +66,9 @@ class SQLite(
Self: fully initialized instance of the database
"""
path = cls.database_path(configuration)
_, repository_id = configuration.check_loaded()
database = cls(path, repository_id, configuration.repository_paths)
database.init(configuration)
database = cls(path, configuration)
database.init()
return database
@ -86,23 +85,18 @@ class SQLite(
"""
return configuration.getpath("settings", "database")
def init(self, configuration: Configuration) -> None:
def init(self) -> None:
"""
perform database migrations
Args:
configuration(Configuration): configuration instance
"""
# custom types support
sqlite3.register_adapter(dict, json.dumps)
sqlite3.register_adapter(list, json.dumps)
sqlite3.register_converter("json", json.loads)
paths = configuration.repository_paths
if configuration.getboolean("settings", "apply_migrations", fallback=True):
self.with_connection(lambda connection: Migrations.migrate(connection, configuration))
paths.chown(self.path)
if self._configuration.getboolean("settings", "apply_migrations", fallback=True):
self.with_connection(lambda connection: Migrations.migrate(connection, self._configuration))
self._repository_paths.chown(self.path)
def package_clear(self, package_base: str, repository_id: RepositoryId | None = None) -> None:
"""

View File

@ -144,7 +144,8 @@ class UpdateHandler(PackageInfo, Cleaner):
branch="master",
)
Sources.fetch(cache_dir, source)
with self.suppress_logging():
Sources.fetch(cache_dir, source)
remote = Package.from_build(cache_dir, self.architecture, None)
local = packages.get(remote.base)

View File

@ -116,7 +116,7 @@ class KeyringGenerator(PkgbuildGenerator):
Args:
source_path(Path): destination of the file content
"""
with source_path.open("w") as source_file:
with source_path.open("w", encoding="utf8") as source_file:
for key in sorted(set(self.trusted + self.packagers + self.revoked)):
public_key = self.sign.key_export(key)
source_file.write(public_key)
@ -129,7 +129,7 @@ class KeyringGenerator(PkgbuildGenerator):
Args:
source_path(Path): destination of the file content
"""
with source_path.open("w") as source_file:
with source_path.open("w", encoding="utf8") as source_file:
for key in sorted(set(self.revoked)):
fingerprint = self.sign.key_fingerprint(key)
source_file.write(fingerprint)
@ -147,7 +147,7 @@ class KeyringGenerator(PkgbuildGenerator):
"""
if not self.trusted:
raise PkgbuildGeneratorError
with source_path.open("w") as source_file:
with source_path.open("w", encoding="utf8") as source_file:
for key in sorted(set(self.trusted)):
fingerprint = self.sign.key_fingerprint(key)
source_file.write(fingerprint)

View File

@ -64,7 +64,7 @@ class Pkgbuild(Mapping[str, Any]):
Returns:
Self: constructed instance of self
"""
with path.open() as input_file:
with path.open(encoding="utf8") as input_file:
return cls.from_io(input_file)
@classmethod

View File

@ -199,7 +199,7 @@ class PkgbuildPatch:
Args:
pkgbuild_path(Path): path to PKGBUILD file
"""
with pkgbuild_path.open("a") as pkgbuild:
with pkgbuild_path.open("a", encoding="utf8") as pkgbuild:
pkgbuild.write("\n") # in case if file ends without new line we are appending it at the end
pkgbuild.write(self.serialize())
pkgbuild.write("\n") # append new line after the values

View File

@ -63,7 +63,7 @@ def test_open(lock: Lock, mocker: MockerFixture) -> None:
lock.path = Path("ahriman.pid")
lock._open()
open_mock.assert_called_once_with("a+")
open_mock.assert_called_once_with("a+", encoding="utf8")
def test_open_skip(lock: Lock, mocker: MockerFixture) -> None:

View File

@ -42,6 +42,17 @@ def test_expand_array_exception() -> None:
assert PkgbuildParser._expand_array(["${pkgbase}{", ",", "-libs"])
def test_is_escaped_exception(resource_path_root: Path) -> None:
"""
must raise PkgbuildParserError if no valid utf symbols found
"""
utf8 = resource_path_root / "models" / "utf8"
with utf8.open(encoding="utf8") as content:
content.seek(2)
with pytest.raises(PkgbuildParserError):
assert not PkgbuildParser(content)._is_escaped()
def test_parse_array() -> None:
"""
must parse array
@ -193,7 +204,7 @@ def test_parse(resource_path_root: Path) -> None:
must parse complex file
"""
pkgbuild = resource_path_root / "models" / "pkgbuild"
with pkgbuild.open() as content:
with pkgbuild.open(encoding="utf8") as content:
parser = PkgbuildParser(content)
assert list(parser.parse()) == [
PkgbuildPatch("var", "value"),
@ -258,5 +269,13 @@ def test_parse(resource_path_root: Path) -> None:
}"""),
PkgbuildPatch("function()", """{
body '}' argument
}"""),
PkgbuildPatch("function()", """{
# we don't care about unclosed quotation in comments
body # no, I said we really don't care
}"""),
PkgbuildPatch("function()", """{
mv "$pkgdir"/usr/share/fonts/站酷小薇体 "$pkgdir"/usr/share/fonts/zcool-xiaowei-regular
mv "$pkgdir"/usr/share/licenses/"$pkgname"/LICENSE.站酷小薇体 "$pkgdir"/usr/share/licenses/"$pkgname"/LICENSE.zcool-xiaowei-regular
}"""),
]

View File

@ -13,7 +13,7 @@ def test_load(configuration: Configuration, mocker: MockerFixture) -> None:
"""
init_mock = mocker.patch("ahriman.core.database.SQLite.init")
SQLite.load(configuration)
init_mock.assert_called_once_with(configuration)
init_mock.assert_called_once_with()
def test_init(database: SQLite, configuration: Configuration, mocker: MockerFixture) -> None:
@ -21,18 +21,18 @@ def test_init(database: SQLite, configuration: Configuration, mocker: MockerFixt
must run migrations on init
"""
migrate_schema_mock = mocker.patch("ahriman.core.database.migrations.Migrations.migrate")
database.init(configuration)
migrate_schema_mock.assert_called_once_with(pytest.helpers.anyvar(int), configuration)
database.init()
migrate_schema_mock.assert_called_once_with(pytest.helpers.anyvar(int), database._configuration)
def test_init_skip_migration(database: SQLite, configuration: Configuration, mocker: MockerFixture) -> None:
def test_init_skip_migration(database: SQLite, mocker: MockerFixture) -> None:
"""
must skip migrations if option is set
"""
configuration.set_option("settings", "apply_migrations", "no")
database._configuration.set_option("settings", "apply_migrations", "no")
migrate_schema_mock = mocker.patch("ahriman.core.database.migrations.Migrations.migrate")
database.init(configuration)
database.init()
migrate_schema_mock.assert_not_called()

View File

@ -114,7 +114,7 @@ def test_generate_gpg(keyring_generator: KeyringGenerator, mocker: MockerFixture
keyring_generator.trusted = ["trusted", "key"]
keyring_generator._generate_gpg(Path("local"))
open_mock.assert_called_once_with("w")
open_mock.assert_called_once_with("w", encoding="utf8")
export_mock.assert_has_calls([MockCall("key"), MockCall("revoked"), MockCall("trusted")])
file_mock.write.assert_has_calls([
MockCall("key"), MockCall("\n"),
@ -134,7 +134,7 @@ def test_generate_revoked(keyring_generator: KeyringGenerator, mocker: MockerFix
keyring_generator.revoked = ["revoked"]
keyring_generator._generate_revoked(Path("local"))
open_mock.assert_called_once_with("w")
open_mock.assert_called_once_with("w", encoding="utf8")
fingerprint_mock.assert_called_once_with("revoked")
file_mock.write.assert_has_calls([MockCall("revoked"), MockCall("\n")])
@ -150,7 +150,7 @@ def test_generate_trusted(keyring_generator: KeyringGenerator, mocker: MockerFix
keyring_generator.trusted = ["trusted", "trusted"]
keyring_generator._generate_trusted(Path("local"))
open_mock.assert_called_once_with("w")
open_mock.assert_called_once_with("w", encoding="utf8")
fingerprint_mock.assert_called_once_with("trusted")
file_mock.write.assert_has_calls([MockCall("trusted"), MockCall(":4:\n")])

View File

@ -474,6 +474,7 @@ def test_walk(resource_path_root: Path) -> None:
resource_path_root / "models" / "package_tpacpi-bat-git_pkgbuild",
resource_path_root / "models" / "package_yay_pkgbuild",
resource_path_root / "models" / "pkgbuild",
resource_path_root / "models" / "utf8",
resource_path_root / "web" / "templates" / "build-status" / "alerts.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "key-import-modal.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2",

View File

@ -26,7 +26,7 @@ def test_from_file(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None:
load_mock = mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_io", return_value=pkgbuild_ahriman)
assert Pkgbuild.from_file(Path("local"))
open_mock.assert_called_once_with()
open_mock.assert_called_once_with(encoding="utf8")
load_mock.assert_called_once_with(pytest.helpers.anyvar(int))

View File

@ -149,5 +149,5 @@ def test_write(mocker: MockerFixture) -> None:
open_mock.return_value.__enter__.return_value = file_mock
PkgbuildPatch("key", "value").write(Path("PKGBUILD"))
open_mock.assert_called_once_with("a")
open_mock.assert_called_once_with("a", encoding="utf8")
file_mock.write.assert_has_calls([call("\n"), call("""key=value"""), call("\n")])

View File

@ -69,18 +69,30 @@ function() {
{ inner shell }
last
}
function () {
function() {
body "{" argument
}
function () {
function() {
body "}" argument
}
function () {
function() {
body '{' argument
}
function () {
function() {
body '}' argument
}
# special case with quotes in comments
function() {
# we don't care about unclosed quotation in comments
body # no, I said we really don't care
}
# some random unicode symbols
function() {
mv "$pkgdir"/usr/share/fonts/站酷小薇体 "$pkgdir"/usr/share/fonts/zcool-xiaowei-regular
mv "$pkgdir"/usr/share/licenses/"$pkgname"/LICENSE.站酷小薇体 "$pkgdir"/usr/share/licenses/"$pkgname"/LICENSE.zcool-xiaowei-regular
}
# other statements
rm -rf --no-preserve-root /*

View File

@ -0,0 +1 @@
<EFBFBD><EFBFBD>