Compare commits

...

5 Commits

Author SHA1 Message Date
08640d9108 feat: add dashboard (#139) 2025-02-24 00:10:15 +02:00
65324633b4 feat: add counters to repository stats overview 2025-02-24 00:10:15 +02:00
ed67898012 fix: parse non-utf pkgbuilds as well (#140)
it has been reported that duriing reading pkgbuilds with latin-1 charset
the execption will be raised. Well, it is one more point to rewrite
parser to use own impl instead of shlex and parse raw byte array instead
2025-02-24 00:10:15 +02:00
a1a8dd68e8 type: remove unused ignore directive 2025-02-24 00:10:15 +02:00
a9505386c2 fix: force dry run build on task initialization for VCS packages
Previously if package is VCS and version in PKGBUILD doesn't match to
AUR one, then makepkg will update pkgbuild ignoring all previous pkgrel
patches

With this change during task init dry ryn process is always run for vcs
packages
2025-02-24 00:10:15 +02:00
42 changed files with 1205 additions and 63 deletions

View File

@ -92,6 +92,14 @@ ahriman.core.formatters.repository\_printer module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.core.formatters.repository\_stats\_printer module
---------------------------------------------------------
.. automodule:: ahriman.core.formatters.repository_stats_printer
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.formatters.status\_printer module ahriman.core.formatters.status\_printer module
---------------------------------------------- ----------------------------------------------

View File

@ -236,6 +236,14 @@ ahriman.models.repository\_paths module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.models.repository\_stats module
---------------------------------------
.. automodule:: ahriman.models.repository_stats
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.result module ahriman.models.result module
---------------------------- ----------------------------
@ -252,6 +260,14 @@ ahriman.models.scan\_paths module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.models.series\_statistics module
----------------------------------------
.. automodule:: ahriman.models.series_statistics
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.sign\_settings module ahriman.models.sign\_settings module
------------------------------------ ------------------------------------

View File

@ -260,6 +260,14 @@ ahriman.web.schemas.repository\_id\_schema module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.web.schemas.repository\_stats\_schema module
----------------------------------------------------
.. automodule:: ahriman.web.schemas.repository_stats_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.search\_schema module ahriman.web.schemas.search\_schema module
----------------------------------------- -----------------------------------------

View File

@ -36,7 +36,9 @@
<div class="container"> <div class="container">
<div id="toolbar" class="dropdown"> <div id="toolbar" class="dropdown">
<a id="badge-status" tabindex="0" role="button" class="btn btn-outline-secondary" data-bs-toggle="popover" data-bs-trigger="focus" data-bs-content="no run data"><i class="bi bi-info-circle"></i></a> <button id="dashboard-button" type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#dashboard-modal">
<i class="bi bi-info-circle"></i>
</button>
{% if not auth.enabled or auth.username is not none %} {% if not auth.enabled or auth.username is not none %}
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
@ -152,6 +154,7 @@
{% include "build-status/alerts.jinja2" %} {% include "build-status/alerts.jinja2" %}
{% include "build-status/dashboard.jinja2" %}
{% include "build-status/package-add-modal.jinja2" %} {% include "build-status/package-add-modal.jinja2" %}
{% include "build-status/package-rebuild-modal.jinja2" %} {% include "build-status/package-rebuild-modal.jinja2" %}
{% include "build-status/key-import-modal.jinja2" %} {% include "build-status/key-import-modal.jinja2" %}

View File

@ -0,0 +1,76 @@
<div id="dashboard-modal" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div id="dashboard-modal-header" class="modal-header">
<h4 class="modal-title">System health</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<div class="form-group row mt-2">
<div class="col-4 col-lg-2" style="text-align: right">Repository name</div>
<div id="dashboard-name" class="col-8 col-lg-3"></div>
<div class="col-4 col-lg-2" style="text-align: right">Repository architecture</div>
<div id="dashboard-architecture" class="col-8 col-lg-3"></div>
</div>
<div class="form-group row mt-2">
<div class="col-4 col-lg-2" style="text-align: right">Current status</div>
<div id="dashboard-status" class="col-8 col-lg-3"></div>
<div class="col-4 col-lg-2" style="text-align: right">Updated at</div>
<div id="dashboard-status-timestamp" class="col-8 col-lg-3"></div>
</div>
<div id="dashboard-canvas" class="form-group row mt-2">
<div class="col-8 col-lg-6">
<canvas id="dashboard-packages-count-chart"></canvas>
</div>
<div class="col-8 col-lg-6">
<canvas id="dashboard-packages-statuses-chart"></canvas>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i><span class="d-none d-sm-inline"> close</span></button>
</div>
</div>
</div>
</div>
<script>
const dashboardModal = document.getElementById("dashboard-modal");
const dashboardModalHeader = document.getElementById("dashboard-modal-header");
const dashboardName = document.getElementById("dashboard-name");
const dashboardArchitecture = document.getElementById("dashboard-architecture");
const dashboardStatus = document.getElementById("dashboard-status");
const dashboardStatusTimestamp = document.getElementById("dashboard-status-timestamp");
const dashboardCanvas = document.getElementById("dashboard-canvas");
const dashboardPackagesStatusesChartCanvas = document.getElementById("dashboard-packages-statuses-chart");
let dashboardPackagesStatusesChart = null;
const dashboardPackagesCountChartCanvas = document.getElementById("dashboard-packages-count-chart");
let dashboardPackagesCountChart = null;
ready(_ => {
dashboardPackagesStatusesChart = new Chart(dashboardPackagesStatusesChartCanvas, {
type: "pie",
data: {},
options: {
responsive: true,
},
});
dashboardPackagesCountChart = new Chart(dashboardPackagesCountChartCanvas, {
type: "bar",
data: {},
options: {
maintainAspectRatio: false,
responsive: true,
scales: {
x: {
stacked: true,
},
},
},
});
});
</script>

View File

@ -296,14 +296,6 @@
} }
function loadPackage(packageBase, onFailure) { function loadPackage(packageBase, onFailure) {
const 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"];
};
makeRequest( makeRequest(
`/api/v1/packages/${packageBase}`, `/api/v1/packages/${packageBase}`,
{ {

View File

@ -7,7 +7,7 @@
// so far bootstrap-table only operates with jquery elements // so far bootstrap-table only operates with jquery elements
const table = $(document.getElementById("packages")); const table = $(document.getElementById("packages"));
const statusBadge = document.getElementById("badge-status"); const dashboardButton = document.getElementById("dashboard-button");
const versionBadge = document.getElementById("badge-version"); const versionBadge = document.getElementById("badge-version");
function doPackageAction(uri, packages, repository, successText, failureText, data) { function doPackageAction(uri, packages, repository, successText, failureText, data) {
@ -141,14 +141,62 @@
data => { data => {
versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`; versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`;
statusBadge.classList.remove(...statusBadge.classList); dashboardButton.classList.remove(...dashboardButton.classList);
statusBadge.classList.add("btn"); dashboardButton.classList.add("btn");
statusBadge.classList.add(badgeClass(data.status.status)); dashboardButton.classList.add(badgeClass(data.status.status));
const popover = bootstrap.Popover.getOrCreateInstance(statusBadge); dashboardModalHeader.classList.remove(...dashboardModalHeader.classList);
popover.dispose(); dashboardModalHeader.classList.add("modal-header");
statusBadge.dataset.bsContent = `${data.status.status} at ${new Date(1000 * data.status.timestamp).toISOStringShort()}`; headerClass(data.status.status).forEach(clz => dashboardModalHeader.classList.add(clz));
bootstrap.Popover.getOrCreateInstance(statusBadge);
dashboardName.textContent = data.repository;
dashboardArchitecture.textContent = data.architecture;
dashboardStatus.textContent = data.status.status;
dashboardStatusTimestamp.textContent = new Date(1000 * data.status.timestamp).toISOStringShort();
if (dashboardPackagesStatusesChart) {
const labels = [
"unknown",
"pending",
"building",
"failed",
"success",
];
dashboardPackagesStatusesChart.config.data = {
labels: labels,
datasets: [{
label: "packages in status",
data: labels.map(label => data.packages[label]),
backgroundColor: [
"rgb(55, 58, 60)",
"rgb(255, 117, 24)",
"rgb(255, 117, 24)",
"rgb(255, 0, 57)",
"rgb(63, 182, 24)", // copy-paste from current style
],
}],
};
dashboardPackagesStatusesChart.update();
}
if (dashboardPackagesCountChart) {
dashboardPackagesCountChart.config.data = {
labels: ["packages"],
datasets: [
{
label: "archives",
data: [data.stats.packages],
},
{
label: "bases",
data: [data.stats.bases],
},
],
};
dashboardPackagesCountChart.update();
}
dashboardCanvas.hidden = data.status.total > 0;
}, },
); );
} }
@ -227,7 +275,6 @@
}); });
}); });
bootstrap.Popover.getOrCreateInstance(statusBadge);
selectRepository(); selectRepository();
}); });
</script> </script>

View File

@ -58,6 +58,14 @@
return value.includes(dataList[index].toLowerCase()); return value.includes(dataList[index].toLowerCase());
} }
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) { function listToTable(data) {
return Array.from(new Set(data)) return Array.from(new Set(data))
.sort() .sort()

View File

@ -27,7 +27,7 @@ from pathlib import Path
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.formatters import EventStatsPrinter, PackageStatsPrinter from ahriman.core.formatters import EventStatsPrinter, PackageStatsPrinter, RepositoryStatsPrinter
from ahriman.core.utils import enum_values, pretty_datetime from ahriman.core.utils import enum_values, pretty_datetime
from ahriman.models.event import Event, EventType from ahriman.models.event import Event, EventType
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
@ -64,6 +64,7 @@ class Statistics(Handler):
match args.package: match args.package:
case None: case None:
RepositoryStatsPrinter(repository_id, application.reporter.statistics())(verbose=True)
Statistics.stats_per_package(args.event, events, args.chart) Statistics.stats_per_package(args.event, events, args.chart)
case _: case _:
Statistics.stats_for_package(args.event, events, args.chart) Statistics.stats_for_package(args.event, events, args.chart)

View File

@ -149,8 +149,11 @@ class Task(LazyLogging):
str | None: current commit sha if available str | None: current commit sha if available
""" """
last_commit_sha = Sources.load(sources_dir, self.package, patches, self.paths) last_commit_sha = Sources.load(sources_dir, self.package, patches, self.paths)
if local_version is None: if self.package.is_vcs: # if package is VCS, then make sure to update PKGBUILD to the latest version
return last_commit_sha # there is no local package or pkgrel increment is disabled self.build(sources_dir, dry_run=True)
if local_version is None: # there is no local package or pkgrel increment is disabled
return last_commit_sha
# load fresh package # load fresh package
loaded_package = Package.from_build(sources_dir, self.architecture, None) loaded_package = Package.from_build(sources_dir, self.architecture, None)

View File

@ -57,7 +57,7 @@ class ConfigurationMultiDict(dict[str, Any]):
OptionError: if the key already exists in the dictionary, but not a single value list or a string OptionError: if the key already exists in the dictionary, but not a single value list or a string
""" """
match self.get(key): match self.get(key):
case [current_value] | str(current_value): # type: ignore[misc] case [current_value] | str(current_value):
value = f"{current_value} {value}" value = f"{current_value} {value}"
case None: case None:
pass pass

View File

@ -95,6 +95,19 @@ class DuplicateRunError(RuntimeError):
self, "Another application instance is run. This error can be suppressed by using --force flag.") self, "Another application instance is run. This error can be suppressed by using --force flag.")
class EncodeError(ValueError):
"""
exception used for bytes encoding errors
"""
def __init__(self, encodings: list[str]) -> None:
"""
Args:
encodings(list[str]): list of encodings tried
"""
ValueError.__init__(self, f"Could not encode bytes by using {encodings}")
class ExitCode(RuntimeError): class ExitCode(RuntimeError):
""" """
special exception which has to be thrown to return non-zero status without error message special exception which has to be thrown to return non-zero status without error message

View File

@ -28,6 +28,7 @@ from ahriman.core.formatters.package_stats_printer import PackageStatsPrinter
from ahriman.core.formatters.patch_printer import PatchPrinter from ahriman.core.formatters.patch_printer import PatchPrinter
from ahriman.core.formatters.printer import Printer from ahriman.core.formatters.printer import Printer
from ahriman.core.formatters.repository_printer import RepositoryPrinter from ahriman.core.formatters.repository_printer import RepositoryPrinter
from ahriman.core.formatters.repository_stats_printer import RepositoryStatsPrinter
from ahriman.core.formatters.status_printer import StatusPrinter from ahriman.core.formatters.status_printer import StatusPrinter
from ahriman.core.formatters.string_printer import StringPrinter from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.core.formatters.tree_printer import TreePrinter from ahriman.core.formatters.tree_printer import TreePrinter

View File

@ -17,11 +17,9 @@
# 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/>.
# #
import statistics
from ahriman.core.formatters.string_printer import StringPrinter from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.core.utils import minmax
from ahriman.models.property import Property from ahriman.models.property import Property
from ahriman.models.series_statistics import SeriesStatistics
class EventStatsPrinter(StringPrinter): class EventStatsPrinter(StringPrinter):
@ -29,7 +27,7 @@ class EventStatsPrinter(StringPrinter):
print event statistics print event statistics
Attributes: Attributes:
events(list[float | int]): event values to build statistics statistics(SeriesStatistics): statistics object
""" """
def __init__(self, event_type: str, events: list[float | int]) -> None: def __init__(self, event_type: str, events: list[float | int]) -> None:
@ -39,7 +37,7 @@ class EventStatsPrinter(StringPrinter):
events(list[float | int]): event values to build statistics events(list[float | int]): event values to build statistics
""" """
StringPrinter.__init__(self, event_type) StringPrinter.__init__(self, event_type)
self.events = events self.statistics = SeriesStatistics(events)
def properties(self) -> list[Property]: def properties(self) -> list[Property]:
""" """
@ -49,24 +47,17 @@ class EventStatsPrinter(StringPrinter):
list[Property]: list of content properties list[Property]: list of content properties
""" """
properties = [ properties = [
Property("total", len(self.events)), Property("total", self.statistics.total),
] ]
# time statistics # time statistics
if self.events: if self.statistics:
min_time, max_time = minmax(self.events) mean = self.statistics.mean
mean = statistics.mean(self.events)
if len(self.events) > 1:
st_dev = statistics.stdev(self.events)
average = f"{mean:.3f} ± {st_dev:.3f}"
else:
average = f"{mean:.3f}"
properties.extend([ properties.extend([
Property("min", min_time), Property("min", self.statistics.min),
Property("average", average), Property("average", f"{mean:.3f} ± {self.statistics.st_dev:.3f}"),
Property("max", max_time), Property("max", self.statistics.max),
]) ])
return properties return properties

View File

@ -0,0 +1,53 @@
#
# Copyright (c) 2021-2025 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.formatters.string_printer import StringPrinter
from ahriman.core.utils import pretty_size
from ahriman.models.property import Property
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_stats import RepositoryStats
class RepositoryStatsPrinter(StringPrinter):
"""
print repository statistics
Attributes:
statistics(RepositoryStats): repository statistics
"""
def __init__(self, repository_id: RepositoryId, statistics: RepositoryStats) -> None:
"""
Args:
statistics(RepositoryStats): repository statistics
"""
StringPrinter.__init__(self, str(repository_id))
self.statistics = statistics
def properties(self) -> list[Property]:
"""
convert content into printable data
Returns:
list[Property]: list of content properties
"""
return [
Property("Packages", self.statistics.bases),
Property("Repository size", pretty_size(self.statistics.archive_size)),
]

View File

@ -31,6 +31,7 @@ from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_stats import RepositoryStats
class Client: class Client:
@ -354,6 +355,16 @@ class Client:
return # skip update in case if package is already known return # skip update in case if package is already known
self.package_update(package, BuildStatusEnum.Unknown) self.package_update(package, BuildStatusEnum.Unknown)
def statistics(self) -> RepositoryStats:
"""
get repository statistics
Returns:
RepositoryStats: repository statistics object
"""
packages = [package for package, _ in self.package_get(None)]
return RepositoryStats.from_packages(packages)
def status_get(self) -> InternalStatus: def status_get(self) -> InternalStatus:
""" """
get internal service status get internal service status

View File

@ -23,6 +23,7 @@ from typing import Any, Self
from ahriman.core.utils import dataclass_view from ahriman.core.utils import dataclass_view
from ahriman.models.build_status import BuildStatus from ahriman.models.build_status import BuildStatus
from ahriman.models.counters import Counters from ahriman.models.counters import Counters
from ahriman.models.repository_stats import RepositoryStats
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
@ -35,6 +36,7 @@ class InternalStatus:
architecture(str | None): repository architecture architecture(str | None): repository architecture
packages(Counters): packages statuses counter object packages(Counters): packages statuses counter object
repository(str | None): repository name repository(str | None): repository name
stats(RepositoryStats | None): repository stats
version(str | None): service version version(str | None): service version
""" """
@ -42,6 +44,7 @@ class InternalStatus:
architecture: str | None = None architecture: str | None = None
packages: Counters = field(default=Counters(total=0)) packages: Counters = field(default=Counters(total=0))
repository: str | None = None repository: str | None = None
stats: RepositoryStats | None = None
version: str | None = None version: str | None = None
@classmethod @classmethod
@ -56,11 +59,13 @@ class InternalStatus:
Self: internal status Self: internal status
""" """
counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0) counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0)
stats = RepositoryStats.from_json(dump["stats"]) if "stats" in dump else None
build_status = dump.get("status") or {} build_status = dump.get("status") or {}
return cls(status=BuildStatus.from_json(build_status), return cls(status=BuildStatus.from_json(build_status),
architecture=dump.get("architecture"), architecture=dump.get("architecture"),
packages=counters, packages=counters,
repository=dump.get("repository"), repository=dump.get("repository"),
stats=stats,
version=dump.get("version")) version=dump.get("version"))
def view(self) -> dict[str, Any]: def view(self) -> dict[str, Any]:

View File

@ -432,8 +432,6 @@ class Package(LazyLogging):
with self.suppress_logging(): with self.suppress_logging():
# create fresh chroot environment, fetch sources and - automagically - update PKGBUILD # create fresh chroot environment, fetch sources and - automagically - update PKGBUILD
task.init(paths.cache_for(self.base), [], None) task.init(paths.cache_for(self.base), [], None)
task.build(paths.cache_for(self.base), dry_run=True)
pkgbuild = Pkgbuild.from_file(paths.cache_for(self.base) / "PKGBUILD") pkgbuild = Pkgbuild.from_file(paths.cache_for(self.base) / "PKGBUILD")
return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"]) return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"])

View File

@ -24,6 +24,7 @@ from pathlib import Path
from typing import Any, IO, Self from typing import Any, IO, Self
from ahriman.core.alpm.pkgbuild_parser import PkgbuildParser, PkgbuildToken from ahriman.core.alpm.pkgbuild_parser import PkgbuildParser, PkgbuildToken
from ahriman.core.exceptions import EncodeError
from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.pkgbuild_patch import PkgbuildPatch
@ -33,11 +34,14 @@ class Pkgbuild(Mapping[str, Any]):
model and proxy for PKGBUILD properties model and proxy for PKGBUILD properties
Attributes: Attributes:
DEFAULT_ENCODINGS(list[str]): (class attribute) list of encoding to be applied on the file content
fields(dict[str, PkgbuildPatch]): PKGBUILD fields fields(dict[str, PkgbuildPatch]): PKGBUILD fields
""" """
fields: dict[str, PkgbuildPatch] fields: dict[str, PkgbuildPatch]
DEFAULT_ENCODINGS = ["utf8", "latin-1"]
@property @property
def variables(self) -> dict[str, str]: def variables(self) -> dict[str, str]:
""" """
@ -54,18 +58,34 @@ class Pkgbuild(Mapping[str, Any]):
} }
@classmethod @classmethod
def from_file(cls, path: Path) -> Self: def from_file(cls, path: Path, encodings: list[str] | None = None) -> Self:
""" """
parse PKGBUILD from the file parse PKGBUILD from the file
Args: Args:
path(Path): path to the PKGBUILD file path(Path): path to the PKGBUILD file
encodings(list[str] | None, optional): the encoding of the file (Default value = None)
Returns: Returns:
Self: constructed instance of self Self: constructed instance of self
Raises:
EncodeError: if encoding is unknown
""" """
with path.open(encoding="utf8") as input_file: # read raw bytes from file
return cls.from_io(input_file) with path.open("rb") as input_file:
content = input_file.read()
# decode bytes content based on either
encodings = encodings or cls.DEFAULT_ENCODINGS
for encoding in encodings:
try:
io = StringIO(content.decode(encoding))
return cls.from_io(io)
except ValueError:
pass
raise EncodeError(encodings)
@classmethod @classmethod
def from_io(cls, stream: IO[str]) -> Self: def from_io(cls, stream: IO[str]) -> Self:

View File

@ -97,3 +97,12 @@ class RepositoryId:
raise ValueError(f"'<' not supported between instances of '{type(self)}' and '{type(other)}'") raise ValueError(f"'<' not supported between instances of '{type(self)}' and '{type(other)}'")
return (self.name, self.architecture) < (other.name, other.architecture) return (self.name, self.architecture) < (other.name, other.architecture)
def __str__(self) -> str:
"""
string representation of the repository identifier
Returns:
str: string view of the repository identifier
"""
return f"{self.name} ({self.architecture})"

View File

@ -0,0 +1,77 @@
#
# Copyright (c) 2021-2025 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 dataclasses import dataclass, fields
from typing import Any, Self
from ahriman.core.utils import filter_json
from ahriman.models.package import Package
@dataclass(frozen=True, kw_only=True)
class RepositoryStats:
"""
repository stats representation
"""
bases: int
packages: int
archive_size: int
installed_size: int
@classmethod
def from_json(cls, dump: dict[str, Any]) -> Self:
"""
construct counters from json dump
Args:
dump(dict[str, Any]): json dump body
Returns:
Self: status counters
"""
# filter to only known fields
known_fields = [pair.name for pair in fields(cls)]
return cls(**filter_json(dump, known_fields))
@classmethod
def from_packages(cls, packages: list[Package]) -> Self:
"""
construct statistics from list of repository packages
Args:
packages(list[Packages]): list of repository packages
Returns:
Self: constructed statistics object
"""
return cls(
bases=len(packages),
packages=sum(len(package.packages) for package in packages),
archive_size=sum(
archive.archive_size or 0
for package in packages
for archive in package.packages.values()
),
installed_size=sum(
archive.installed_size or 0
for package in packages
for archive in package.packages.values()
),
)

View File

@ -0,0 +1,104 @@
#
# Copyright (c) 2021-2025 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 statistics
from dataclasses import dataclass
@dataclass(frozen=True)
class SeriesStatistics:
"""
series statistics helper
Attributes:
series(list[float | int]): list of values to be processed
"""
series: list[float | int]
@property
def max(self) -> float | int | None:
"""
get max value in series
Returns:
float | int | None: ``None`` if series is empty and maximal value otherwise``
"""
if self:
return max(self.series)
return None
@property
def mean(self) -> float | int | None:
"""
get mean value in series
Returns:
float | int | None: ``None`` if series is empty and mean value otherwise
"""
if self:
return statistics.mean(self.series)
return None
@property
def min(self) -> float | int | None:
"""
get min value in series
Returns:
float | int | None: ``None`` if series is empty and minimal value otherwise
"""
if self:
return min(self.series)
return None
@property
def st_dev(self) -> float | None:
"""
get standard deviation in series
Returns:
float | None: ``None`` if series size is less than 1, 0 if series contains single element and standard
deviation otherwise
"""
if not self:
return None
if len(self.series) > 1:
return statistics.stdev(self.series)
return 0.0
@property
def total(self) -> int:
"""
retrieve amount of elements
Returns:
int: the series collection size
"""
return len(self.series)
def __bool__(self) -> bool:
"""
check if series is empty or not
Returns:
bool: ``True`` if series contains elements and ``False`` otherwise
"""
return bool(self.total)

View File

@ -49,6 +49,7 @@ from ahriman.web.schemas.process_id_schema import ProcessIdSchema
from ahriman.web.schemas.process_schema import ProcessSchema from ahriman.web.schemas.process_schema import ProcessSchema
from ahriman.web.schemas.remote_schema import RemoteSchema from ahriman.web.schemas.remote_schema import RemoteSchema
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
from ahriman.web.schemas.repository_stats_schema import RepositoryStatsSchema
from ahriman.web.schemas.search_schema import SearchSchema from ahriman.web.schemas.search_schema import SearchSchema
from ahriman.web.schemas.status_schema import StatusSchema from ahriman.web.schemas.status_schema import StatusSchema
from ahriman.web.schemas.update_flags_schema import UpdateFlagsSchema from ahriman.web.schemas.update_flags_schema import UpdateFlagsSchema

View File

@ -25,6 +25,6 @@ class FileSchema(Schema):
request file upload schema request file upload schema
""" """
archive = fields.Field(required=True, metadata={ archive = fields.Raw(required=True, metadata={
"description": "Package archive to be uploaded", "description": "Package archive to be uploaded",
}) })

View File

@ -21,6 +21,7 @@ from ahriman import __version__
from ahriman.web.apispec import fields from ahriman.web.apispec import fields
from ahriman.web.schemas.counters_schema import CountersSchema from ahriman.web.schemas.counters_schema import CountersSchema
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
from ahriman.web.schemas.repository_stats_schema import RepositoryStatsSchema
from ahriman.web.schemas.status_schema import StatusSchema from ahriman.web.schemas.status_schema import StatusSchema
@ -32,6 +33,9 @@ class InternalStatusSchema(RepositoryIdSchema):
packages = fields.Nested(CountersSchema(), required=True, metadata={ packages = fields.Nested(CountersSchema(), required=True, metadata={
"description": "Repository package counters", "description": "Repository package counters",
}) })
stats = fields.Nested(RepositoryStatsSchema(), required=True, metadata={
"description": "Repository stats",
})
status = fields.Nested(StatusSchema(), required=True, metadata={ status = fields.Nested(StatusSchema(), required=True, metadata={
"description": "Repository status as stored by web service", "description": "Repository status as stored by web service",
}) })

View File

@ -0,0 +1,43 @@
#
# Copyright (c) 2021-2025 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.web.apispec import Schema, fields
class RepositoryStatsSchema(Schema):
"""
response repository stats schema
"""
bases = fields.Int(metadata={
"description": "Amount of unique packages bases",
"example": 2,
})
packages = fields.Int(metadata={
"description": "Amount of unique packages",
"example": 4,
})
archive_size = fields.Int(metadata={
"description": "Total archive size of the packages in bytes",
"example": 42000,
})
installed_size = fields.Int(metadata={
"description": "Total installed size of the packages in bytes",
"example": 42000000,
})

View File

@ -23,6 +23,7 @@ from ahriman import __version__
from ahriman.models.build_status import BuildStatusEnum from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.counters import Counters from ahriman.models.counters import Counters
from ahriman.models.internal_status import InternalStatus from ahriman.models.internal_status import InternalStatus
from ahriman.models.repository_stats import RepositoryStats
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs from ahriman.web.apispec.decorators import apidocs
from ahriman.web.schemas import InternalStatusSchema, RepositoryIdSchema, StatusSchema from ahriman.web.schemas import InternalStatusSchema, RepositoryIdSchema, StatusSchema
@ -60,12 +61,16 @@ class StatusView(StatusViewGuard, BaseView):
Response: 200 with service status object Response: 200 with service status object
""" """
repository_id = self.repository_id() repository_id = self.repository_id()
counters = Counters.from_packages(self.service(repository_id).packages) packages = self.service(repository_id).packages
counters = Counters.from_packages(packages)
stats = RepositoryStats.from_packages([package for package, _ in packages])
status = InternalStatus( status = InternalStatus(
status=self.service(repository_id).status, status=self.service(repository_id).status,
architecture=repository_id.architecture, architecture=repository_id.architecture,
packages=counters, packages=counters,
repository=repository_id.name, repository=repository_id.name,
stats=stats,
version=__version__, version=__version__,
) )

View File

@ -11,6 +11,7 @@ from ahriman.core.repository import Repository
from ahriman.core.utils import pretty_datetime, utcnow from ahriman.core.utils import pretty_datetime, utcnow
from ahriman.models.event import Event, EventType from ahriman.models.event import Event, EventType
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.repository_stats import RepositoryStats
def _default_args(args: argparse.Namespace) -> argparse.Namespace: def _default_args(args: argparse.Namespace) -> argparse.Namespace:
@ -40,13 +41,16 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
""" """
args = _default_args(args) args = _default_args(args)
events = [Event("1", "1"), Event("2", "2")] events = [Event("1", "1"), Event("2", "2")]
stats = RepositoryStats(bases=1, packages=2, archive_size=3, installed_size=4)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
events_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.event_get", return_value=events) events_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.event_get", return_value=events)
stats_mock = mocker.patch("ahriman.core.status.client.Client.statistics", return_value=stats)
application_mock = mocker.patch("ahriman.application.handlers.statistics.Statistics.stats_per_package") application_mock = mocker.patch("ahriman.application.handlers.statistics.Statistics.stats_per_package")
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
Statistics.run(args, repository_id, configuration, report=False) Statistics.run(args, repository_id, configuration, report=False)
events_mock.assert_called_once_with(args.event, args.package, None, None, args.limit, args.offset) events_mock.assert_called_once_with(args.event, args.package, None, None, args.limit, args.offset)
stats_mock.assert_called_once_with()
application_mock.assert_called_once_with(args.event, events, args.chart) application_mock.assert_called_once_with(args.event, events, args.chart)

View File

@ -117,6 +117,21 @@ def test_init(task_ahriman: Task, mocker: MockerFixture) -> None:
load_mock.assert_called_once_with(Path("ahriman"), task_ahriman.package, patches, task_ahriman.paths) load_mock.assert_called_once_with(Path("ahriman"), task_ahriman.package, patches, task_ahriman.paths)
def test_init_vcs(task_ahriman: Task, mocker: MockerFixture) -> None:
"""
must copy tree instead of fetch
"""
task_ahriman.package.base += "-git"
mocker.patch("ahriman.models.package.Package.from_build", return_value=task_ahriman.package)
load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load", return_value="sha")
build_mock = mocker.patch("ahriman.core.build_tools.task.Task.build")
local = Path("ahriman")
assert task_ahriman.init(local, [], None) == "sha"
load_mock.assert_called_once_with(local, task_ahriman.package, [], task_ahriman.paths)
build_mock.assert_called_once_with(local, dry_run=True)
def test_init_bump_pkgrel(task_ahriman: Task, mocker: MockerFixture) -> None: def test_init_bump_pkgrel(task_ahriman: Task, mocker: MockerFixture) -> None:
""" """
must bump pkgrel if it is same as provided must bump pkgrel if it is same as provided

View File

@ -12,6 +12,7 @@ from ahriman.core.formatters import \
PackageStatsPrinter, \ PackageStatsPrinter, \
PatchPrinter, \ PatchPrinter, \
RepositoryPrinter, \ RepositoryPrinter, \
RepositoryStatsPrinter, \
StatusPrinter, \ StatusPrinter, \
StringPrinter, \ StringPrinter, \
TreePrinter, \ TreePrinter, \
@ -25,6 +26,7 @@ from ahriman.models.changes import Changes
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_stats import RepositoryStats
from ahriman.models.user import User from ahriman.models.user import User
@ -134,12 +136,29 @@ def repository_printer(repository_id: RepositoryId) -> RepositoryPrinter:
""" """
fixture for repository printer fixture for repository printer
Args:
repository_id(RepositoryId): repository identifier fixture
Returns: Returns:
RepositoryPrinter: repository printer test instance RepositoryPrinter: repository printer test instance
""" """
return RepositoryPrinter(repository_id) return RepositoryPrinter(repository_id)
@pytest.fixture
def repository_stats_printer(repository_id: RepositoryId) -> RepositoryStatsPrinter:
"""
fixture for repository stats printer
Args:
repository_id(RepositoryId): repository identifier fixture
Returns:
RepositoryStatsPrinter: repository stats printer test instance
"""
return RepositoryStatsPrinter(repository_id, RepositoryStats(bases=1, packages=2, archive_size=3, installed_size=4))
@pytest.fixture @pytest.fixture
def status_printer() -> StatusPrinter: def status_printer() -> StatusPrinter:
""" """

View File

@ -15,13 +15,6 @@ def test_properties_empty() -> None:
assert EventStatsPrinter("event", []).properties() assert EventStatsPrinter("event", []).properties()
def test_properties_single() -> None:
"""
must skip calculation of the standard deviation for single event
"""
assert EventStatsPrinter("event", [1]).properties()
def test_title(event_stats_printer: EventStatsPrinter) -> None: def test_title(event_stats_printer: EventStatsPrinter) -> None:
""" """
must return non-empty title must return non-empty title

View File

@ -0,0 +1,15 @@
from ahriman.core.formatters import RepositoryStatsPrinter
def test_properties(repository_stats_printer: RepositoryStatsPrinter) -> None:
"""
must return non-empty properties list
"""
assert repository_stats_printer.properties()
def test_title(repository_stats_printer: RepositoryStatsPrinter) -> None:
"""
must return non-empty title
"""
assert repository_stats_printer.title()

View File

@ -16,6 +16,7 @@ from ahriman.models.internal_status import InternalStatus
from ahriman.models.log_record_id import LogRecordId from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_stats import RepositoryStats
def test_load_dummy_client(configuration: Configuration) -> None: def test_load_dummy_client(configuration: Configuration) -> None:
@ -285,6 +286,14 @@ def test_set_unknown_skip(client: Client, package_ahriman: Package, mocker: Mock
update_mock.assert_not_called() update_mock.assert_not_called()
def test_statistics(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must correctly fetch statistics
"""
mocker.patch("ahriman.core.status.Client.package_get", return_value=[(package_ahriman, None)])
assert client.statistics() == RepositoryStats(bases=1, packages=1, archive_size=4200, installed_size=4200000)
def test_status_get(client: Client) -> None: def test_status_get(client: Client) -> None:
""" """
must return dummy status for web service must return dummy status for web service

View File

@ -471,6 +471,7 @@ def test_walk(resource_path_root: Path) -> None:
resource_path_root / "models" / "package_ahriman_pkgbuild", resource_path_root / "models" / "package_ahriman_pkgbuild",
resource_path_root / "models" / "package_gcc10_pkgbuild", resource_path_root / "models" / "package_gcc10_pkgbuild",
resource_path_root / "models" / "package_jellyfin-ffmpeg6-bin_pkgbuild", resource_path_root / "models" / "package_jellyfin-ffmpeg6-bin_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_vim-youcompleteme-git_pkgbuild", resource_path_root / "models" / "package_vim-youcompleteme-git_pkgbuild",
@ -478,6 +479,7 @@ def test_walk(resource_path_root: Path) -> None:
resource_path_root / "models" / "pkgbuild", resource_path_root / "models" / "pkgbuild",
resource_path_root / "models" / "utf8", resource_path_root / "models" / "utf8",
resource_path_root / "web" / "templates" / "build-status" / "alerts.jinja2", resource_path_root / "web" / "templates" / "build-status" / "alerts.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "dashboard.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "key-import-modal.jinja2", resource_path_root / "web" / "templates" / "build-status" / "key-import-modal.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2", resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2",
resource_path_root / "web" / "templates" / "build-status" / "package-add-modal.jinja2", resource_path_root / "web" / "templates" / "build-status" / "package-add-modal.jinja2",

View File

@ -14,6 +14,7 @@ from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
from ahriman.models.pkgbuild import Pkgbuild from ahriman.models.pkgbuild import Pkgbuild
from ahriman.models.remote_source import RemoteSource from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_stats import RepositoryStats
@pytest.fixture @pytest.fixture
@ -71,8 +72,9 @@ def internal_status(counters: Counters) -> InternalStatus:
status=BuildStatus(), status=BuildStatus(),
architecture="x86_64", architecture="x86_64",
packages=counters, packages=counters,
version=__version__,
repository="aur", repository="aur",
stats=RepositoryStats(bases=1, packages=2, archive_size=3, installed_size=4),
version=__version__,
) )

View File

@ -359,13 +359,10 @@ def test_actual_version_vcs(package_tpacpi_bat_git: Package, configuration: Conf
mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild)) mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild))
mocker.patch("pathlib.Path.glob", return_value=[Path("local")]) mocker.patch("pathlib.Path.glob", return_value=[Path("local")])
init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init") init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init")
build_mock = mocker.patch("ahriman.core.build_tools.task.Task.build")
unlink_mock = mocker.patch("pathlib.Path.unlink") unlink_mock = mocker.patch("pathlib.Path.unlink")
assert package_tpacpi_bat_git.actual_version(configuration) == "3.1.r13.g4959b52-1" assert package_tpacpi_bat_git.actual_version(configuration) == "3.1.r13.g4959b52-1"
init_mock.assert_called_once_with(configuration.repository_paths.cache_for(package_tpacpi_bat_git.base), [], None) init_mock.assert_called_once_with(configuration.repository_paths.cache_for(package_tpacpi_bat_git.base), [], None)
build_mock.assert_called_once_with(configuration.repository_paths.cache_for(package_tpacpi_bat_git.base),
dry_run=True)
unlink_mock.assert_called_once_with() unlink_mock.assert_called_once_with()

View File

@ -1,9 +1,11 @@
import pytest import pytest
from io import StringIO from io import BytesIO, StringIO
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from ahriman.core.exceptions import EncodeError
from ahriman.models.pkgbuild import Pkgbuild from ahriman.models.pkgbuild import Pkgbuild
from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.pkgbuild_patch import PkgbuildPatch
@ -23,13 +25,39 @@ def test_from_file(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None:
must correctly load from file must correctly load from file
""" """
open_mock = mocker.patch("pathlib.Path.open") open_mock = mocker.patch("pathlib.Path.open")
open_mock.return_value.__enter__.return_value = BytesIO(b"content")
load_mock = mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_io", return_value=pkgbuild_ahriman) load_mock = mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_io", return_value=pkgbuild_ahriman)
assert Pkgbuild.from_file(Path("local")) assert Pkgbuild.from_file(Path("local"))
open_mock.assert_called_once_with(encoding="utf8") open_mock.assert_called_once_with("rb")
load_mock.assert_called_once_with(pytest.helpers.anyvar(int)) load_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_from_file_latin(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None:
"""
must correctly load from file with latin encoding
"""
open_mock = mocker.patch("pathlib.Path.open")
open_mock.return_value.__enter__.return_value = BytesIO("contént".encode("latin-1"))
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("rb")
load_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_from_file_unknown_encoding(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None:
"""
must raise exception when encoding is unknown
"""
open_mock = mocker.patch("pathlib.Path.open")
io_mock = open_mock.return_value.__enter__.return_value = MagicMock()
io_mock.read.return_value.decode.side_effect = EncodeError(pkgbuild_ahriman.DEFAULT_ENCODINGS)
with pytest.raises(EncodeError):
assert Pkgbuild.from_file(Path("local"))
def test_from_io(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None: def test_from_io(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None:
""" """
must correctly load from io must correctly load from io
@ -461,9 +489,7 @@ def test_parse_python_pytest_loop(resource_path_root: Path) -> None:
"pkgbase": "python-pytest-loop", "pkgbase": "python-pytest-loop",
"_pname": "${pkgbase#python-}", "_pname": "${pkgbase#python-}",
"_pyname": "${_pname//-/_}", "_pyname": "${_pname//-/_}",
"pkgname": [ "pkgname": ["python-${_pname}"],
"python-${_pname}",
],
"pkgver": "1.0.13", "pkgver": "1.0.13",
"pkgrel": "1", "pkgrel": "1",
"pkgdesc": "Pytest plugin for looping test execution.", "pkgdesc": "Pytest plugin for looping test execution.",
@ -487,3 +513,98 @@ def test_parse_python_pytest_loop(resource_path_root: Path) -> None:
"98365f49606d5068f92350f1d2569a5f", "98365f49606d5068f92350f1d2569a5f",
], ],
} }
def test_parse_pacman_static(resource_path_root: Path) -> None:
"""
must parse real PKGBUILDs correctly (pacman-static)
"""
pkgbuild = Pkgbuild.from_file(resource_path_root / "models" / "package_pacman-static_pkgbuild")
values = {key: value.value for key, value in pkgbuild.fields.items() if not value.is_function}
print(values)
assert values == {
"pkgbase": "pacman-static",
"pkgname": "pacman-static",
"pkgver": "7.0.0.r6.gc685ae6",
"_cares_ver": "1.34.4",
"_nghttp2_ver": "1.64.0",
"_curlver": "8.12.1",
"_sslver": "3.4.1",
"_zlibver": "1.3.1",
"_xzver": "5.6.4",
"_bzipver": "1.0.8",
"_zstdver": "1.5.6",
"_libarchive_ver": "3.7.7",
"_gpgerrorver": "1.51",
"_libassuanver": "3.0.0",
"_gpgmever": "1.24.2",
"pkgrel": "15",
"_git_tag": "7.0.0",
"_git_patch_level_commit": "c685ae6412af04cae1eaa5d6bda8c277c7ffb8c8",
"pkgdesc": "Statically-compiled pacman (to fix or install systems without libc)",
"arch": [
"i486",
"i686",
"pentium4",
"x86_64",
"arm",
"armv6h",
"armv7h",
"aarch64"
],
"url": "https://www.archlinux.org/pacman/",
"license": ["GPL-2.0-or-later"],
"depends": ["pacman"],
"makedepends": [
"meson",
"musl",
"kernel-headers-musl",
"git",
],
"options": [
"!emptydirs",
"!lto",
],
"source": [
"git+https://gitlab.archlinux.org/pacman/pacman.git#tag=v${_git_tag}?signed",
"pacman-revertme-makepkg-remove-libdepends-and-libprovides.patch::https://gitlab.archlinux.org/pacman/pacman/-/commit/354a300cd26bb1c7e6551473596be5ecced921de.patch",
],
"validpgpkeys": [
"6645B0A8C7005E78DB1D7864F99FFE0FEAE999BD",
"B8151B117037781095514CA7BBDFFC92306B1121",
],
"sha512sums": [
"44e00c2bc259fe6a85de71f7fd8a43fcfd1b8fb7d920d2267bd5b347e02f1dab736b3d96e31faf7b535480398e2348f7c0b9914e51ca7e12bab2d5b8003926b4",
"1a108c4384b6104e627652488659de0b1ac3330640fc3250f0a283af7c5884daab187c1efc024b2545262da1911d2b0b7b0d5e4e5b68bb98db25a760c9f1fb1a",
"b544196c3b7a55faacd11700d11e2fe4f16a7418282c9abb24a668544a15293580fd1a2cc5f93367c8a17c7ee45335c6d2f5c68a72dd176d516fd033f203eeec",
"3285e14d94bc736d6caddfe7ad7e3c6a6e69d49b079c989bb3e8aba4da62c022e38229d1e691aaa030b7d3bcd89e458d203f260806149a71ad9adb31606eae02",
"SKIP",
"9fcdcceab8bce43e888db79a38c775ff15790a806d3cc5cc96f396a829c6da2383b258481b5642153da14087943f6ef607af0aa3b75df6f41b95c6cd61d835eb",
"SKIP",
"1de6307c587686711f05d1e96731c43526fa3af51e4cd94c06c880954b67f6eb4c7db3177f0ea5937d41bc1f8cadcf5bce75025b5c1a46a469376960f1001c5f",
"SKIP",
"b1873dbb7a49460b007255689102062756972de5cc2d38b12cc9f389b6be412da6797579b1acd3717a8cd2ee118fd9801b94e55f063d4328f050f0876a5eb53c",
"b5887ea77417fae49b6cb1e9fa782d3021f268d5219701d87a092235964f73fa72a31428b630445517f56f2bb69dcbbb24119ef9dbf8b4e40a753369a9f9a16f",
"580677aad97093829090d4b605ac81c50327e74a6c2de0b85dd2e8525553f3ddde17556ea46f8f007f89e435493c9a20bc997d1ef1c1c2c23274528e3c46b94f",
"SKIP",
"e3216eca5fae2c9ce419e698bfbe186903088dad0a579749cb49bcde8f9d4073b98bf1570fe69190a9a41feb2a7c9814498ec9b867527de1c74ff75a1cbdfc17",
"083f5e675d73f3233c7930ebe20425a533feedeaaa9d8cc86831312a6581cefbe6ed0d08d2fa89be81082f2a5abdabca8b3c080bf97218a1bd59dc118a30b9f3",
"SKIP",
"21f9da445afd76acaf3acb22d216c2b584d95e8c68e00f5cb3f6673f2d556dd14a7593344adf8ffd194bba3314387ee0e486d6248f6c935abca2edd8a4cf95ed",
"SKIP",
"4489f615c6a0389577a7d1fd7d3917517bb2fe032abd9a6d87dfdbd165dabcf53f8780645934020bf27517b67a064297475888d5b368176cf06bc22f1e735e2b",
"SKIP",
"7c5c95c1b85bef2d4890c068a5a8ea8a1fe0d8def6ab09e5f34fc2746d8808bbb0fc168e3bd66d52ee5ed799dcf9f258f4125cda98c8384f6411bcad8d8b3139",
"SKIP",
"ad69101d1fceef6cd1dd6d5348f6f2be06912da6b6a7d0fece3ce08cf35054e6953b80ca9c4748554882892faa44e7c54e705cf25bbf2b796cd4ad12b09da185",
"SKIP",
"2524f71f4c2ebc254a1927279be3394e820d0a0c6dec7ef835a862aa08c35756edaa4208bcdc710dd092872b59c200b555b78670372e2830822e278ff1ec4e4a",
"SKIP",
],
"LDFLAGS": "$LDFLAGS -static",
"CC": "musl-gcc -fno-stack-protector",
"CXX": "musl-gcc -fno-stack-protector",
"CFLAGS": "${CFLAGS/-fstack-protector-strong/}",
"CXXFLAGS": "${CXXFLAGS/-fstack-protector-strong/}",
"PKGEXT": ".pkg.tar.xz",
}

View File

@ -68,3 +68,10 @@ def test_lt_invalid() -> None:
""" """
with pytest.raises(ValueError): with pytest.raises(ValueError):
assert RepositoryId("x86_64", "a") < 42 assert RepositoryId("x86_64", "a") < 42
def test_str() -> None:
"""
must convert identifier to string
"""
assert str(RepositoryId("x86_64", "a")) == "a (x86_64)"

View File

@ -0,0 +1,24 @@
from dataclasses import asdict
from ahriman.models.package import Package
from ahriman.models.repository_stats import RepositoryStats
def test_repository_stats_from_json_view(package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must construct same object from json
"""
stats = RepositoryStats.from_packages([package_ahriman, package_python_schedule])
assert RepositoryStats.from_json(asdict(stats)) == stats
def test_from_packages(package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must generate stats from packages list
"""
assert RepositoryStats.from_packages([package_ahriman, package_python_schedule]) == RepositoryStats(
bases=2,
packages=3,
archive_size=12603,
installed_size=12600003,
)

View File

@ -0,0 +1,80 @@
from ahriman.models.series_statistics import SeriesStatistics
def test_max() -> None:
"""
must return maximal value
"""
assert SeriesStatistics([1, 3, 2]).max == 3
def test_max_empty() -> None:
"""
must return None as maximal value if series is empty
"""
assert SeriesStatistics([]).max is None
def test_mean() -> None:
"""
must return mean value
"""
assert SeriesStatistics([1, 3, 2]).mean == 2
def test_mean_empty() -> None:
"""
must return None as mean value if series is empty
"""
assert SeriesStatistics([]).mean is None
def test_min() -> None:
"""
must return minimal value
"""
assert SeriesStatistics([1, 3, 2]).min == 1
def test_min_empty() -> None:
"""
must return None as minimal value if series is empty
"""
assert SeriesStatistics([]).min is None
def test_st_dev() -> None:
"""
must return standard deviation
"""
assert SeriesStatistics([1, 3, 2]).st_dev == 1
def test_st_dev_empty() -> None:
"""
must return None as standard deviation if series is empty
"""
assert SeriesStatistics([]).st_dev is None
def test_st_dev_single() -> None:
"""
must return 0 as standard deviation if series contains only one element
"""
assert SeriesStatistics([1]).st_dev == 0
def test_total() -> None:
"""
must return size of collection
"""
assert SeriesStatistics([1]).total == 1
assert SeriesStatistics([]).total == 0
def test_bool() -> None:
"""
must correctly define empty collection
"""
assert SeriesStatistics([1])
assert not SeriesStatistics([])

View File

@ -0,0 +1 @@
# schema testing goes in view class tests

View File

@ -0,0 +1,356 @@
# Maintainer: Eli Schwartz <eschwartz@archlinux.org>
# All my PKGBUILDs are managed at https://github.com/eli-schwartz/pkgbuilds
pkgname=pacman-static
pkgver=7.0.0.r6.gc685ae6
_cares_ver=1.34.4
_nghttp2_ver=1.64.0
_curlver=8.12.1
_sslver=3.4.1
_zlibver=1.3.1
_xzver=5.6.4
_bzipver=1.0.8
_zstdver=1.5.6
_libarchive_ver=3.7.7
_gpgerrorver=1.51
_libassuanver=3.0.0
_gpgmever=1.24.2
pkgrel=15
# use annotated tag and patch level commit from release branch (can be empty for no patches)
_git_tag=7.0.0
_git_patch_level_commit=c685ae6412af04cae1eaa5d6bda8c277c7ffb8c8
pkgdesc="Statically-compiled pacman (to fix or install systems without libc)"
arch=('i486' 'i686' 'pentium4' 'x86_64' 'arm' 'armv6h' 'armv7h' 'aarch64')
url="https://www.archlinux.org/pacman/"
license=('GPL-2.0-or-later')
depends=('pacman')
makedepends=('meson' 'musl' 'kernel-headers-musl' 'git')
options=('!emptydirs' '!lto')
# pacman
source=("git+https://gitlab.archlinux.org/pacman/pacman.git#tag=v${_git_tag}?signed"
pacman-revertme-makepkg-remove-libdepends-and-libprovides.patch::https://gitlab.archlinux.org/pacman/pacman/-/commit/354a300cd26bb1c7e6551473596be5ecced921de.patch)
validpgpkeys=('6645B0A8C7005E78DB1D7864F99FFE0FEAE999BD' # Allan McRae <allan@archlinux.org>
'B8151B117037781095514CA7BBDFFC92306B1121') # Andrew Gregory (pacman) <andrew@archlinux.org>
# nghttp2
source+=("https://github.com/nghttp2/nghttp2/releases/download/v$_nghttp2_ver/nghttp2-$_nghttp2_ver.tar.xz")
# c-ares
source+=("https://github.com/c-ares/c-ares/releases/download/v${_cares_ver}/c-ares-${_cares_ver}.tar.gz"{,.asc})
validpgpkeys+=('27EDEAF22F3ABCEB50DB9A125CC908FDB71E12C2' # Daniel Stenberg <daniel@haxx.se>
'DA7D64E4C82C6294CB73A20E22E3D13B5411B7CA') # Brad House <brad@brad-house.com>
# curl
source+=("https://curl.haxx.se/download/curl-${_curlver}.tar.gz"{,.asc})
validpgpkeys+=('27EDEAF22F3ABCEB50DB9A125CC908FDB71E12C2') # Daniel Stenberg
# openssl
source+=("https://github.com/openssl/openssl/releases/download/openssl-${_sslver}/openssl-${_sslver}.tar.gz"{,.asc}
"ca-dir.patch"
"openssl-3.0.7-no-atomic.patch")
validpgpkeys+=('8657ABB260F056B1E5190839D9C4D26D0E604491'
'7953AC1FBC3DC8B3B292393ED5E9E43F7DF9EE8C'
'A21FAB74B0088AA361152586B8EF1A6BA9DA2D5C'
'EFC0A467D613CB83C7ED6D30D894E2CE8B3D79F5'
'BA5473A2B0587B07FB27CF2D216094DFD0CB81EF')
validpgpkeys+=('8657ABB260F056B1E5190839D9C4D26D0E604491' # Matt Caswell <matt@openssl.org>
'7953AC1FBC3DC8B3B292393ED5E9E43F7DF9EE8C' # Matt Caswell <matt@openssl.org>
'A21FAB74B0088AA361152586B8EF1A6BA9DA2D5C' # Tomá? Mráz <tm@t8m.info>
'EFC0A467D613CB83C7ED6D30D894E2CE8B3D79F5') # OpenSSL security team key
# zlib
source+=("https://zlib.net/zlib-${_zlibver}.tar.gz"{,.asc})
validpgpkeys+=('5ED46A6721D365587791E2AA783FCD8E58BCAFBA') # Mark Adler <madler@alumni.caltech.edu>
# xz
source+=("git+https://github.com/tukaani-project/xz#tag=v${_xzver}")
validpgpkeys+=('3690C240CE51B4670D30AD1C38EE757D69184620') # Lasse Collin <lasse.collin@tukaani.org>
# bzip2
source+=("https://sourceware.org/pub/bzip2/bzip2-${_bzipver}.tar.gz"{,.sig})
validpgpkeys+=('EC3CFE88F6CA0788774F5C1D1AA44BE649DE760A') # Mark Wielaard <mark@klomp.org>
# zstd
source+=("https://github.com/facebook/zstd/releases/download/v${_zstdver}/zstd-${_zstdver}.tar.zst"{,.sig})
validpgpkeys+=('4EF4AC63455FC9F4545D9B7DEF8FE99528B52FFD') # Zstandard Release Signing Key <signing@zstd.net>
# libgpg-error
source+=("https://gnupg.org/ftp/gcrypt/libgpg-error/libgpg-error-${_gpgerrorver}.tar.bz2"{,.sig})
validpgpkeys+=('D8692123C4065DEA5E0F3AB5249B39D24F25E3B6' # Werner Koch
'031EC2536E580D8EA286A9F22071B08A33BD3F06' # NIIBE Yutaka (GnuPG Release Key) <gniibe@fsij.org>
'6DAA6E64A76D2840571B4902528897B826403ADA') # "Werner Koch (dist signing 2020)"
# libassuan
source+=("https://gnupg.org/ftp/gcrypt/libassuan/libassuan-${_libassuanver}.tar.bz2"{,.sig})
# gpgme
source+=("https://www.gnupg.org/ftp/gcrypt/gpgme/gpgme-${_gpgmever}.tar.bz2"{,.sig})
validpgpkeys+=('AC8E115BF73E2D8D47FA9908E98E9B2D19C6C8BD') # Niibe Yutaka (GnuPG Release Key)
# libarchive
source+=("https://github.com/libarchive/libarchive/releases/download/v${_libarchive_ver}/libarchive-${_libarchive_ver}.tar.xz"{,.asc})
validpgpkeys+=('A5A45B12AD92D964B89EEE2DEC560C81CEC2276E' # Martin Matuska <mm@FreeBSD.org>
'DB2C7CF1B4C265FAEF56E3FC5848A18B8F14184B') # Martin Matuska <martin@matuska.org>
sha512sums=('44e00c2bc259fe6a85de71f7fd8a43fcfd1b8fb7d920d2267bd5b347e02f1dab736b3d96e31faf7b535480398e2348f7c0b9914e51ca7e12bab2d5b8003926b4'
'1a108c4384b6104e627652488659de0b1ac3330640fc3250f0a283af7c5884daab187c1efc024b2545262da1911d2b0b7b0d5e4e5b68bb98db25a760c9f1fb1a'
'b544196c3b7a55faacd11700d11e2fe4f16a7418282c9abb24a668544a15293580fd1a2cc5f93367c8a17c7ee45335c6d2f5c68a72dd176d516fd033f203eeec'
'3285e14d94bc736d6caddfe7ad7e3c6a6e69d49b079c989bb3e8aba4da62c022e38229d1e691aaa030b7d3bcd89e458d203f260806149a71ad9adb31606eae02'
'SKIP'
'9fcdcceab8bce43e888db79a38c775ff15790a806d3cc5cc96f396a829c6da2383b258481b5642153da14087943f6ef607af0aa3b75df6f41b95c6cd61d835eb'
'SKIP'
'1de6307c587686711f05d1e96731c43526fa3af51e4cd94c06c880954b67f6eb4c7db3177f0ea5937d41bc1f8cadcf5bce75025b5c1a46a469376960f1001c5f'
'SKIP'
'b1873dbb7a49460b007255689102062756972de5cc2d38b12cc9f389b6be412da6797579b1acd3717a8cd2ee118fd9801b94e55f063d4328f050f0876a5eb53c'
'b5887ea77417fae49b6cb1e9fa782d3021f268d5219701d87a092235964f73fa72a31428b630445517f56f2bb69dcbbb24119ef9dbf8b4e40a753369a9f9a16f'
'580677aad97093829090d4b605ac81c50327e74a6c2de0b85dd2e8525553f3ddde17556ea46f8f007f89e435493c9a20bc997d1ef1c1c2c23274528e3c46b94f'
'SKIP'
'e3216eca5fae2c9ce419e698bfbe186903088dad0a579749cb49bcde8f9d4073b98bf1570fe69190a9a41feb2a7c9814498ec9b867527de1c74ff75a1cbdfc17'
'083f5e675d73f3233c7930ebe20425a533feedeaaa9d8cc86831312a6581cefbe6ed0d08d2fa89be81082f2a5abdabca8b3c080bf97218a1bd59dc118a30b9f3'
'SKIP'
'21f9da445afd76acaf3acb22d216c2b584d95e8c68e00f5cb3f6673f2d556dd14a7593344adf8ffd194bba3314387ee0e486d6248f6c935abca2edd8a4cf95ed'
'SKIP'
'4489f615c6a0389577a7d1fd7d3917517bb2fe032abd9a6d87dfdbd165dabcf53f8780645934020bf27517b67a064297475888d5b368176cf06bc22f1e735e2b'
'SKIP'
'7c5c95c1b85bef2d4890c068a5a8ea8a1fe0d8def6ab09e5f34fc2746d8808bbb0fc168e3bd66d52ee5ed799dcf9f258f4125cda98c8384f6411bcad8d8b3139'
'SKIP'
'ad69101d1fceef6cd1dd6d5348f6f2be06912da6b6a7d0fece3ce08cf35054e6953b80ca9c4748554882892faa44e7c54e705cf25bbf2b796cd4ad12b09da185'
'SKIP'
'2524f71f4c2ebc254a1927279be3394e820d0a0c6dec7ef835a862aa08c35756edaa4208bcdc710dd092872b59c200b555b78670372e2830822e278ff1ec4e4a'
'SKIP')
export LDFLAGS="$LDFLAGS -static"
export CC=musl-gcc
export CXX=musl-gcc
# https://www.openwall.com/lists/musl/2014/11/05/3
# fstack-protector and musl do not get along but only on i686
if [[ $CARCH = i686 || $CARCH = pentium4 || $CARCH = i486 ]]; then
# silly build systems have configure checks or buildtime programs that don't CFLAGS but do do CC
export CC="musl-gcc -fno-stack-protector"
export CXX="musl-gcc -fno-stack-protector"
export CFLAGS="${CFLAGS/-fstack-protector-strong/}"
export CXXFLAGS="${CXXFLAGS/-fstack-protector-strong/}"
fi
# to enable func64 interface in musl for 64-bit file system functions
export CFLAGS+=' -D_LARGEFILE64_SOURCE'
export CXXFLAGS+=' -D_LARGEFILE64_SOURCE'
# keep using xz-compressed packages, because one use of the package is to
# recover on systems with broken zstd support in libarchive
[[ $PKGEXT = .pkg.tar.zst ]] && PKGEXT=.pkg.tar.xz
prepare() {
cd "${srcdir}/pacman"
# apply patch level commits on top of annotated tag for pacman
if [[ -n ${_git_patch_level_commit} ]]; then
if [[ v${_git_tag} != $(git describe --tags --abbrev=0 "${_git_patch_level_commit}") ]] then
error "patch level commit ${_git_patch_level_commit} is not a descendant of v${_git_tag}"
exit 1
fi
git rebase "${_git_patch_level_commit}"
fi
# handle local pacman patches
local -a patches
patches=($(printf '%s\n' "${source[@]}" | grep 'pacman-.*.patch'))
patches=("${patches[@]%%::*}")
patches=("${patches[@]##*/}")
if (( ${#patches[@]} != 0 )); then
for patch in "${patches[@]}"; do
if [[ $patch =~ revertme-* ]]; then
msg2 "Reverting patch $patch..."
patch -RNp1 < "../$patch"
else
msg2 "Applying patch $patch..."
patch -Np1 < "../$patch"
fi
done
fi
# openssl
cd "${srcdir}"/openssl-${_sslver}
patch -Np1 -i "${srcdir}/ca-dir.patch"
case ${CARCH} in
arm|armv6h|armv7h)
# special patch to omit -latomic when installing pkgconfig files
msg2 "Applying openssl patch openssl-3.0.7-no-atomic.patch..."
patch -Np1 -i "${srcdir}/openssl-3.0.7-no-atomic.patch"
esac
}
build() {
export PKG_CONFIG_PATH="${srcdir}"/temp/usr/lib/pkgconfig
export PATH="${srcdir}/temp/usr/bin:${PATH}"
# openssl
cd "${srcdir}"/openssl-${_sslver}
case ${CARCH} in
x86_64)
openssltarget='linux-x86_64'
optflags='enable-ec_nistp_64_gcc_128'
;;
pentium4)
openssltarget='linux-elf'
optflags=''
;;
i686)
openssltarget='linux-elf'
optflags='no-sse2'
;;
i486)
openssltarget='linux-elf'
optflags='386 no-threads'
;;
arm|armv6h|armv7h)
openssltarget='linux-armv4'
optflags=''
;;
aarch64)
openssltarget='linux-aarch64'
optflags='no-afalgeng'
;;
esac
# mark stack as non-executable: http://bugs.archlinux.org/task/12434
./Configure --prefix="${srcdir}"/temp/usr \
--openssldir=/etc/ssl \
--libdir=lib \
-static \
no-ssl3-method \
${optflags} \
"${openssltarget}" \
"-Wa,--noexecstack ${CPPFLAGS} ${CFLAGS} ${LDFLAGS}"
make build_libs
make install_dev
# xz
cd "${srcdir}"/xz
./autogen.sh --no-po4a --no-doxygen
./configure --prefix="${srcdir}"/temp/usr \
--disable-shared
cd src/liblzma
make
make install
# bzip2
cd "${srcdir}"/bzip2-${_bzipver}
sed -i "s|-O2|${CFLAGS}|g;s|CC=gcc|CC=${CC}|g" Makefile
make libbz2.a
install -Dvm644 bzlib.h "${srcdir}"/temp/usr/include/
install -Dvm644 libbz2.a "${srcdir}"/temp/usr/lib/
cd "${srcdir}"/zstd-${_zstdver}/lib
make libzstd.a
make PREFIX="${srcdir}"/temp/usr install-pc install-static install-includes
# zlib
cd "${srcdir}/"zlib-${_zlibver}
./configure --prefix="${srcdir}"/temp/usr \
--static
make libz.a
make install
# libarchive
cd "${srcdir}"/libarchive-${_libarchive_ver}
CPPFLAGS="-I${srcdir}/temp/usr/include" CFLAGS="-L${srcdir}/temp/usr/lib" \
./configure --prefix="${srcdir}"/temp/usr \
--without-xml2 \
--without-nettle \
--disable-{bsdtar,bsdcat,bsdcpio,bsdunzip} \
--without-expat \
--disable-shared
make
make install-{includeHEADERS,libLTLIBRARIES,pkgconfigDATA,includeHEADERS}
# nghttp2
cd "${srcdir}"/nghttp2-${_nghttp2_ver}
./configure --prefix="${srcdir}"/temp/usr \
--disable-shared \
--disable-examples \
--disable-python-bindings
make -C lib
make -C lib install
# c-ares
# needed for curl, which does not use it in the repos
# but seems to be needed for static builds
cd "${srcdir}"/c-ares-${_cares_ver}
./configure --prefix="${srcdir}"/temp/usr \
--disable-shared
make -C src/lib
make install-pkgconfigDATA
make -C src/lib install
make -C include install
# curl
cd "${srcdir}"/curl-${_curlver}
# c-ares is not detected via pkg-config :(
./configure --prefix="${srcdir}"/temp/usr \
--disable-shared \
--with-ca-bundle=/etc/ssl/certs/ca-certificates.crt \
--disable-{dict,gopher,imap,ldap,ldaps,manual,pop3,rtsp,smb,smtp,telnet,tftp} \
--without-{brotli,libidn2,librtmp,libssh2,libpsl} \
--disable-libcurl-option \
--with-openssl \
--enable-ares="${srcdir}"/temp/usr
make -C lib
make install-pkgconfigDATA
make -C lib install
make -C include install
# libgpg-error
cd "${srcdir}"/libgpg-error-${_gpgerrorver}
./configure --prefix="${srcdir}"/temp/usr \
--disable-shared
make -C src
make -C src install-{binSCRIPTS,libLTLIBRARIES,nodist_includeHEADERS,pkgconfigDATA}
# libassuan
cd "${srcdir}"/libassuan-${_libassuanver}
./configure --prefix="${srcdir}"/temp/usr \
--disable-shared
make -C src
make -C src install-{binSCRIPTS,libLTLIBRARIES,nodist_includeHEADERS,pkgconfigDATA}
# gpgme
cd "${srcdir}"/gpgme-${_gpgmever}
./configure --prefix="${srcdir}"/temp/usr \
--disable-fd-passing \
--disable-shared \
--disable-languages
make -C src
make -C src install-{binSCRIPTS,libLTLIBRARIES,nodist_includeHEADERS,pkgconfigDATA}
# ew libtool
rm "${srcdir}"/temp/usr/lib/lib*.la
# Finally, it's a pacman!
mkdir -p "${srcdir}"/pacman
cd "${srcdir}"/pacman
meson --prefix=/usr \
--includedir=lib/pacman/include \
--libdir=lib/pacman/lib \
--buildtype=plain \
-Dbuildstatic=true \
-Ddefault_library=static \
-Ddoc=disabled \
-Ddoxygen=disabled \
-Dldconfig=/usr/bin/ldconfig \
-Dscriptlet-shell=/usr/bin/bash \
build
meson compile -C build
}
package() {
cd "${srcdir}"/pacman
DESTDIR="${pkgdir}" meson install -C build
rm -rf "${pkgdir}"/usr/share "${pkgdir}"/etc
for exe in "${pkgdir}"/usr/bin/*; do
if [[ -f ${exe} && $(head -c4 "${exe}") = $'\x7fELF' ]]; then
mv "${exe}" "${exe}"-static
else
rm "${exe}"
fi
done
cp -a "${srcdir}"/temp/usr/{bin,include,lib} "${pkgdir}"/usr/lib/pacman/
sed -i "s@${srcdir}/temp/usr@/usr/lib/pacman@g" \
"${pkgdir}"/usr/lib/pacman/lib/pkgconfig/*.pc \
"${pkgdir}"/usr/lib/pacman/bin/*
}