mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-06-27 14:22:10 +00:00
Compare commits
59 Commits
Author | SHA1 | Date | |
---|---|---|---|
646190121a | |||
10e4f3b629 | |||
80a1f37c85 | |||
751676a07e | |||
e1a7071ce5 | |||
1605d185e2 | |||
2fdf910e78 | |||
63dc43366b | |||
74a244f06c | |||
69499b2d0a | |||
475afe4e08 | |||
2f2d6c2b70 | |||
15e3d2500c | |||
3d74b1485a | |||
413d3b7509 | |||
3e2fb7b4e6 | |||
71196dc58b | |||
e7736e985f | |||
f929a552e8 | |||
2c7ef3471e | |||
47bb22b1f4 | |||
5a340146bb | |||
0937a9a4b5 | |||
310eba694f | |||
22d2057c3a | |||
47fc5bca57 | |||
d4222eca25 | |||
b5046b787c | |||
75c0cc970e | |||
504d57b2f5 | |||
4c20d0241a | |||
db0a6bf34e | |||
8f5af7965e | |||
f35278e978 | |||
a288986450 | |||
2cef540cc0 | |||
7fd22e9f16 | |||
e2608db991 | |||
2a0311b042 | |||
0fcb46afca | |||
374b3febc8 | |||
3e0b3cdbaa | |||
663e822a92 | |||
d3e79120cb | |||
67b97a64ea | |||
7ace74af44 | |||
b7d481858d | |||
f753563804 | |||
4727894349 | |||
4b98b21a70 | |||
9410c521a1 | |||
dd42cd0cd6 | |||
50b409cd3e | |||
356cd35c5f | |||
3405105dce | |||
4445c8c871 | |||
a3a66c7c9a | |||
45b762e3d9 | |||
c5db7e64ca |
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
### Steps to Reproduce
|
||||
|
||||
Steps to reproduce the behavior (commands, environment etc)
|
||||
|
||||
### Expected behavior
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
### Logs
|
||||
|
||||
Add logs to help explain your problem. Logs to stderr can be generated by using `--no-log` command line option.
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
12
.github/ISSUE_TEMPLATE/discussion.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/discussion.md
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
name: Question
|
||||
about: Create an issue to get help with project
|
||||
title: ''
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Describe your question below
|
||||
|
||||
A clear and concise description of your issue for which you would like to get help.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Feature summary
|
||||
|
||||
Brief description of the feature required
|
||||
|
||||
### Cause of the feature request
|
||||
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
### Proposed changes and/or features
|
||||
|
||||
A clear and concise description of what you want to happen.
|
13
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
13
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
## Summary
|
||||
|
||||
Brief description of the pull request. Try to provide clear explanation for major changes.
|
||||
|
||||
Please make sure that branch called either `feature/feature-name` for feature-related pull requests or `bug/bug-name` for bug-related ones.
|
||||
|
||||
Put `closes #ISSUE` in case if the pull requests solves one of the opened issues.
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Tests to cover new code
|
||||
- [ ] `make check` passed
|
||||
- [ ] `make tests` passed
|
27
.github/workflows/python-app.yml
vendored
Normal file
27
.github/workflows/python-app.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
# based on https://github.com/actions/starter-workflows/blob/main/ci/python-app.yml
|
||||
|
||||
name: ahriman
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: run check and tests in archlinux container
|
||||
run: |
|
||||
docker run \
|
||||
-v ${{ github.workspace }}:/build -w /build \
|
||||
archlinux:latest \
|
||||
/bin/bash -c "pacman --noconfirm -Syu base-devel python python-pip && \
|
||||
pip install -e .[web] && \
|
||||
pip install -e .[check] && \
|
||||
pip install -e .[test] && \
|
||||
make check tests"
|
608
.pylintrc
Normal file
608
.pylintrc
Normal file
@ -0,0 +1,608 @@
|
||||
[MASTER]
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Specify a score threshold to be exceeded before program exits with error.
|
||||
fail-under=10.0
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use.
|
||||
jobs=1
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python module names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=print-statement,
|
||||
parameter-unpacking,
|
||||
unpacking-in-except,
|
||||
old-raise-syntax,
|
||||
backtick,
|
||||
long-suffix,
|
||||
old-ne-operator,
|
||||
old-octal-literal,
|
||||
import-star-module-level,
|
||||
non-ascii-bytes-literal,
|
||||
raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
file-ignored,
|
||||
suppressed-message,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
apply-builtin,
|
||||
basestring-builtin,
|
||||
buffer-builtin,
|
||||
cmp-builtin,
|
||||
coerce-builtin,
|
||||
execfile-builtin,
|
||||
file-builtin,
|
||||
long-builtin,
|
||||
raw_input-builtin,
|
||||
reduce-builtin,
|
||||
standarderror-builtin,
|
||||
unicode-builtin,
|
||||
xrange-builtin,
|
||||
coerce-method,
|
||||
delslice-method,
|
||||
getslice-method,
|
||||
setslice-method,
|
||||
no-absolute-import,
|
||||
old-division,
|
||||
dict-iter-method,
|
||||
dict-view-method,
|
||||
next-method-called,
|
||||
metaclass-assignment,
|
||||
indexing-exception,
|
||||
raising-string,
|
||||
reload-builtin,
|
||||
oct-method,
|
||||
hex-method,
|
||||
nonzero-method,
|
||||
cmp-method,
|
||||
input-builtin,
|
||||
round-builtin,
|
||||
intern-builtin,
|
||||
unichr-builtin,
|
||||
map-builtin-not-iterating,
|
||||
zip-builtin-not-iterating,
|
||||
range-builtin-not-iterating,
|
||||
filter-builtin-not-iterating,
|
||||
using-cmp-argument,
|
||||
eq-without-hash,
|
||||
div-method,
|
||||
idiv-method,
|
||||
rdiv-method,
|
||||
exception-message-attribute,
|
||||
invalid-str-codec,
|
||||
sys-max-int,
|
||||
bad-python3-import,
|
||||
deprecated-string-function,
|
||||
deprecated-str-translate-call,
|
||||
deprecated-itertools-function,
|
||||
deprecated-types-field,
|
||||
next-method-defined,
|
||||
dict-items-not-iterating,
|
||||
dict-keys-not-iterating,
|
||||
dict-values-not-iterating,
|
||||
deprecated-operator-function,
|
||||
deprecated-urllib-function,
|
||||
xreadlines-attribute,
|
||||
deprecated-sys-function,
|
||||
exception-escape,
|
||||
comprehension-escape,
|
||||
missing-module-docstring,
|
||||
line-too-long,
|
||||
no-name-in-module,
|
||||
import-outside-toplevel,
|
||||
invalid-name,
|
||||
raise-missing-from,
|
||||
wrong-import-order,
|
||||
too-few-public-methods,
|
||||
too-many-instance-attributes,
|
||||
broad-except,
|
||||
logging-fstring-interpolation,
|
||||
too-many-ancestors,
|
||||
fixme,
|
||||
too-many-arguments,
|
||||
duplicate-code,
|
||||
cyclic-import
|
||||
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=c-extension-no-member
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a score less than or equal to 10. You
|
||||
# have access to the variables 'error', 'warning', 'refactor', and 'convention'
|
||||
# which contain the number of messages in each category, as well as 'statement'
|
||||
# which is the total number of statements analyzed. This score is used by the
|
||||
# global evaluation report (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
#msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style.
|
||||
#argument-rgx=
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style.
|
||||
#attr-rgx=
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Bad variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be refused
|
||||
bad-names-rgxs=
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style.
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style.
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style.
|
||||
#const-rgx=
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style.
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=i,
|
||||
j,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
_
|
||||
|
||||
# Good variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be accepted
|
||||
good-names-rgxs=
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style.
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style.
|
||||
#method-rgx=
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style.
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style.
|
||||
#variable-rgx=
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis). It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
# List of decorators that change the signature of a decorated function.
|
||||
signature-mutators=
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# The type of string formatting that logging methods do. `old` means using %
|
||||
# formatting, `new` is for `{}` formatting.
|
||||
logging-format-style=old
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
# Regular expression of note tags to take in consideration.
|
||||
#notes-rgx=
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it work,
|
||||
# install the python-enchant package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains the private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to the private dictionary (see the
|
||||
# --spelling-private-dict-file option) instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid defining new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||
|
||||
|
||||
[STRING]
|
||||
|
||||
# This flag controls whether inconsistent-quotes generates a warning when the
|
||||
# character used as a quote delimiter is used inconsistently within a module.
|
||||
check-quote-consistency=no
|
||||
|
||||
# This flag controls whether the implicit-str-concat should generate a warning
|
||||
# on implicit string concatenation in sequences defined over several lines.
|
||||
check-str-concat-over-line-jumps=no
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# List of modules that can be imported at any level, not just the top level
|
||||
# one.
|
||||
allow-any-import-level=
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=optparse,tkinter.tix
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled).
|
||||
import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
# Couples of modules and preferred modules, separated by a comma.
|
||||
preferred-modules=
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement (see R0916).
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# Warn about protected attribute access inside special methods
|
||||
check-protected-access-in-special-methods=no
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp,
|
||||
__post_init__
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,
|
||||
_fields,
|
||||
_replace,
|
||||
_source,
|
||||
_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=cls
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "BaseException, Exception".
|
||||
overgeneral-exceptions=BaseException,
|
||||
Exception
|
2
AUTHORS
Normal file
2
AUTHORS
Normal file
@ -0,0 +1,2 @@
|
||||
Current developers:
|
||||
Evgenii Alekseev aka arcanis <esalexeev (at) gmail (dot) com>
|
53
Makefile
Normal file
53
Makefile
Normal file
@ -0,0 +1,53 @@
|
||||
.PHONY: archive archive_directory archlinux check clean directory push tests version
|
||||
.DEFAULT_GOAL := archlinux
|
||||
|
||||
PROJECT := ahriman
|
||||
|
||||
FILES := AUTHORS COPYING CONFIGURING.md README.md package src setup.py
|
||||
TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES))
|
||||
IGNORE_FILES := package/archlinux src/.mypy_cache
|
||||
|
||||
$(TARGET_FILES) : $(addprefix $(PROJECT), %) : $(addprefix ., %) directory version
|
||||
@cp -rp $< $@
|
||||
|
||||
archive: archive_directory
|
||||
tar cJf "$(PROJECT)-$(VERSION)-src.tar.xz" "$(PROJECT)"
|
||||
rm -rf "$(PROJECT)"
|
||||
|
||||
archive_directory: $(TARGET_FILES)
|
||||
rm -fr $(addprefix $(PROJECT)/, $(IGNORE_FILES))
|
||||
find "$(PROJECT)" -type f -name "*.pyc" -delete
|
||||
find "$(PROJECT)" -depth -type d -name "__pycache__" -execdir rm -rf {} +
|
||||
find "$(PROJECT)" -depth -type d -name "*.egg-info" -execdir rm -rf {} +
|
||||
|
||||
archlinux: archive
|
||||
sed -i "/sha512sums=('[0-9A-Fa-f]*/s/[^'][^)]*/sha512sums=('$$(sha512sum $(PROJECT)-$(VERSION)-src.tar.xz | awk '{print $$1}')'/" package/archlinux/PKGBUILD
|
||||
sed -i "s/pkgver=[0-9.]*/pkgver=$(VERSION)/" package/archlinux/PKGBUILD
|
||||
|
||||
check:
|
||||
cd src && mypy --implicit-reexport --strict -p "$(PROJECT)"
|
||||
find "src/$(PROJECT)" tests -name "*.py" -execdir autopep8 --exit-code --max-line-length 120 -aa -i {} +
|
||||
cd src && pylint --rcfile=../.pylintrc "$(PROJECT)"
|
||||
|
||||
clean:
|
||||
find . -type f -name "$(PROJECT)-*-src.tar.xz" -delete
|
||||
rm -rf "$(PROJECT)"
|
||||
|
||||
directory: clean
|
||||
mkdir "$(PROJECT)"
|
||||
|
||||
push: archlinux
|
||||
git add package/archlinux/PKGBUILD src/ahriman/version.py
|
||||
git commit -m "Release $(VERSION)"
|
||||
git push
|
||||
git tag "$(VERSION)"
|
||||
git push --tags
|
||||
|
||||
tests:
|
||||
python setup.py test
|
||||
|
||||
version:
|
||||
ifndef VERSION
|
||||
$(error VERSION is required, but not set)
|
||||
endif
|
||||
sed -i '/__version__ = "[0-9.]*/s/[^"][^)]*/__version__ = "$(VERSION)"/' src/ahriman/version.py
|
61
README.md
61
README.md
@ -15,13 +15,56 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
|
||||
## Installation and run
|
||||
|
||||
* Install package as usual.
|
||||
* Change settings if required, see `CONFIGURING.md` for more details.
|
||||
* Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`).
|
||||
* Configure build tools (it might be required if your package will use any custom repositories):
|
||||
* create build command, e.g. `ln -s /usr/bin/archbuild /usr/local/bin/custom-x86_64-build` (you can choose any name for command);
|
||||
* create configuration file, e.g. `cp /usr/share/devtools/pacman-{extra,custom}.conf`;
|
||||
* Change settings if required, see [CONFIGURING](CONFIGURING.md) for more details.
|
||||
* Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`):
|
||||
|
||||
```shell
|
||||
echo 'PACKAGER="John Doe <john@doe.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf
|
||||
```
|
||||
|
||||
* Configure build tools (it is required for correct dependency management system):
|
||||
|
||||
* create build command, e.g. `ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build` (you can choose any name for command, basically it should be `{name}-{arch}-build`);
|
||||
* create configuration file, e.g. `cp /usr/share/devtools/pacman-{extra,ahriman}.conf` (same as previous `pacman-{name}.conf`);
|
||||
* change configuration file, add your own repository, add multilib repository etc;
|
||||
* set `build.build_command` setting to point to your command;
|
||||
* configure `/etc/sudoers.d/ahriman` to allow running command without password.
|
||||
* Start and enable `ahriman.timer` via `systemctl`.
|
||||
* Add packages by using `ahriman add {package}` command.
|
||||
* set `build_command` option to point to your command;
|
||||
* configure `/etc/sudoers.d/ahriman` to allow running command without a password.
|
||||
|
||||
```shell
|
||||
ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build
|
||||
cp /usr/share/devtools/pacman-{extra,ahriman}.conf
|
||||
|
||||
echo '[multilib]' | tee -a /usr/share/devtools/pacman-ahriman.conf
|
||||
echo 'Include = /etc/pacman.d/mirrorlist' | tee -a /usr/share/devtools/pacman-ahriman.conf
|
||||
|
||||
echo '[aur-clone]' | tee -a /usr/share/devtools/pacman-ahriman.conf
|
||||
echo 'SigLevel = Optional TrustAll' | tee -a /usr/share/devtools/pacman-ahriman.conf
|
||||
echo 'Server = file:///var/lib/ahriman/repository/$arch' | tee -a /usr/share/devtools/pacman-ahriman.conf
|
||||
|
||||
echo '[build]' | tee -a /etc/ahriman.ini.d/build.ini
|
||||
echo 'build_command = ahriman-x86_64-build' | tee -a /etc/ahriman.ini.d/build.ini
|
||||
|
||||
echo 'Cmnd_Alias CARCHBUILD_CMD = /usr/local/bin/ahriman-x86_64-build *' | tee -a /etc/sudoers.d/ahriman
|
||||
echo 'ahriman ALL=(ALL) NOPASSWD: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman
|
||||
chmod 400 /etc/sudoers.d/ahriman
|
||||
```
|
||||
|
||||
* Start and enable `ahriman@.timer` via `systemctl`:
|
||||
|
||||
```shell
|
||||
systemctl enable --now ahriman@x86_64.timer
|
||||
```
|
||||
|
||||
* Start and enable status page:
|
||||
|
||||
```shell
|
||||
systemctl enable --now ahriman-web@x86_64
|
||||
```
|
||||
|
||||
* Add packages by using `ahriman add {package}` command:
|
||||
|
||||
```shell
|
||||
sudo -u ahriman ahriman -a x86_64 add yay
|
||||
```
|
||||
|
||||
Note that initial service configuration can be done by running `ahriman setup` with specific arguments.
|
||||
|
@ -1,35 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
VERSION="$1"
|
||||
ARCHIVE="ahriman"
|
||||
FILES="COPYING CONFIGURING.md README.md package src setup.py"
|
||||
IGNORELIST="build .idea package/archlinux package/*src.tar.xz"
|
||||
|
||||
# set version
|
||||
sed -i "/__version__ = '[0-9.]*/s/[^'][^)]*/__version__ = '$VERSION'/" src/ahriman/version.py
|
||||
|
||||
# create archive
|
||||
[[ -e $ARCHIVE-$VERSION-src.tar.xz ]] && rm -f "$ARCHIVE-$VERSION-src.tar.xz"
|
||||
[[ -d $ARCHIVE ]] && rm -rf "$ARCHIVE"
|
||||
mkdir "$ARCHIVE"
|
||||
for FILE in ${FILES[*]}; do cp -r "$FILE" "$ARCHIVE"; done
|
||||
for FILE in ${IGNORELIST[*]}; do rm -rf "$ARCHIVE/$FILE"; done
|
||||
tar cJf "$ARCHIVE-$VERSION-src.tar.xz" "$ARCHIVE"
|
||||
rm -rf "$ARCHIVE"
|
||||
|
||||
# update checksums
|
||||
SHA512SUMS=$(sha512sum $ARCHIVE-$VERSION-src.tar.xz | awk '{print $1}')
|
||||
sed -i "/sha512sums=('[0-9A-Fa-f]*/s/[^'][^)]*/sha512sums=('$SHA512SUMS'/" package/archlinux/PKGBUILD
|
||||
sed -i "s/pkgver=[0-9.]*/pkgver=$VERSION/" package/archlinux/PKGBUILD
|
||||
|
||||
# clear
|
||||
find . -type f -name '*src.tar.xz' -not -name "*$VERSION-src.tar.xz" -exec rm -f {} \;
|
||||
|
||||
read -p "Publish release? [Ny] " -n 1 -r
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
git add package/archlinux/PKGBUILD src/ahriman/version.py
|
||||
git commit -m "Release $VERSION" && git push
|
||||
git tag "$VERSION" && git push --tags
|
||||
fi
|
@ -1,7 +1,7 @@
|
||||
# Maintainer: Evgeniy Alekseev
|
||||
|
||||
pkgname='ahriman'
|
||||
pkgver=0.11.1
|
||||
pkgver=0.17.0
|
||||
pkgrel=1
|
||||
pkgdesc="ArcHlinux ReposItory MANager"
|
||||
arch=('any')
|
||||
@ -23,7 +23,7 @@ optdepends=('aws-cli: sync to s3'
|
||||
source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz"
|
||||
'ahriman.sysusers'
|
||||
'ahriman.tmpfiles')
|
||||
sha512sums=('91993951dd97c73a323e67e2d0e1aa8072f2863f4cc4281083d15ada724f531ffc8c60610b39810ddd612e16213f59c3c104feb48238372091eb25c7a77d6d02'
|
||||
sha512sums=('0e29a7a075cbca0259a2f6374a5d1d09285112f6221d62b1c24b56ad23807133001d56ed045307866f9c7cde772f54adda234bdfcaaa272ac7d363c7da6d1f9f'
|
||||
'13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075'
|
||||
'55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4')
|
||||
backup=('etc/ahriman.ini'
|
||||
|
@ -42,6 +42,4 @@ remote =
|
||||
bucket =
|
||||
|
||||
[web]
|
||||
host =
|
||||
port =
|
||||
templates = /usr/share/ahriman
|
@ -11,28 +11,28 @@ keys = generic_format
|
||||
class = StreamHandler
|
||||
level = DEBUG
|
||||
formatter = generic_format
|
||||
args = (sys.stdout,)
|
||||
args = (sys.stderr,)
|
||||
|
||||
[handler_file_handler]
|
||||
class = logging.handlers.RotatingFileHandler
|
||||
level = DEBUG
|
||||
formatter = generic_format
|
||||
args = ('/var/log/ahriman/ahriman.log', 'a', 20971520, 20)
|
||||
args = ("/var/log/ahriman/ahriman.log", "a", 20971520, 20)
|
||||
|
||||
[handler_build_file_handler]
|
||||
class = logging.handlers.RotatingFileHandler
|
||||
level = DEBUG
|
||||
formatter = generic_format
|
||||
args = ('/var/log/ahriman/build.log', 'a', 20971520, 20)
|
||||
args = ("/var/log/ahriman/build.log", "a", 20971520, 20)
|
||||
|
||||
[handler_http_handler]
|
||||
class = logging.handlers.RotatingFileHandler
|
||||
level = DEBUG
|
||||
formatter = generic_format
|
||||
args = ('/var/log/ahriman/http.log', 'a', 20971520, 20)
|
||||
args = ("/var/log/ahriman/http.log", "a", 20971520, 20)
|
||||
|
||||
[formatter_generic_format]
|
||||
format = %(asctime)s : %(levelname)s : %(funcName)s : %(message)s
|
||||
format = [%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
|
||||
datefmt =
|
||||
|
||||
[logger_root]
|
||||
|
@ -3,84 +3,52 @@
|
||||
<head>
|
||||
<title>{{ repository|e }}</title>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--color-building: 250, 255, 146;
|
||||
--color-failed: 255, 94, 94;
|
||||
--color-pending: 250, 255, 146;
|
||||
--color-success: 121, 255, 94;
|
||||
--color-unknown: 197, 197, 197;
|
||||
}
|
||||
{% include "style.jinja2" %}
|
||||
|
||||
@keyframes blink-building {
|
||||
0% { background-color: rgba(var(--color-building), 1.0); }
|
||||
10% { background-color: rgba(var(--color-building), 0.9); }
|
||||
20% { background-color: rgba(var(--color-building), 0.8); }
|
||||
30% { background-color: rgba(var(--color-building), 0.7); }
|
||||
40% { background-color: rgba(var(--color-building), 0.6); }
|
||||
50% { background-color: rgba(var(--color-building), 0.5); }
|
||||
60% { background-color: rgba(var(--color-building), 0.4); }
|
||||
70% { background-color: rgba(var(--color-building), 0.3); }
|
||||
80% { background-color: rgba(var(--color-building), 0.2); }
|
||||
90% { background-color: rgba(var(--color-building), 0.1); }
|
||||
100% { background-color: rgba(var(--color-building), 0.0); }
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
padding: 5px;
|
||||
}
|
||||
td.package {
|
||||
font-weight: bolder;
|
||||
}
|
||||
td.package-unknown {
|
||||
background-color: rgba(var(--color-unknown), 1.0);
|
||||
}
|
||||
td.package-pending {
|
||||
background-color: rgba(var(--color-pending), 1.0);
|
||||
}
|
||||
td.package-building {
|
||||
background-color: rgba(var(--color-building), 1.0);
|
||||
animation-name: blink-building;
|
||||
animation-duration: 1s;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
td.package-failed {
|
||||
background-color: rgba(var(--color-failed), 1.0);
|
||||
}
|
||||
td.package-success {
|
||||
background-color: rgba(var(--color-success), 1.0);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://www.kryogenix.org/code/browser/sorttable/sorttable.js"></script>
|
||||
{% include "sorttable.jinja2" %}
|
||||
{% include "search.jinja2" %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>ahriman {{ version|e }}</h1>
|
||||
<div class="root">
|
||||
<h1>ahriman
|
||||
<img src="https://img.shields.io/badge/version-{{ version|e }}-informational" alt="{{ version|e }}">
|
||||
<img src="https://img.shields.io/badge/architecture-{{ architecture|e }}-informational" alt="{{ architecture|e }}">
|
||||
<img src="https://img.shields.io/badge/service%20status-{{ service.status|e }}-{{ service.status_color|e }}" alt="{{ service.status|e }}" title="{{ service.timestamp|e }}">
|
||||
</h1>
|
||||
|
||||
<table class="sortable" id="builds">
|
||||
<tr>
|
||||
<th>package base</th>
|
||||
<th>packages</th>
|
||||
<th>version</th>
|
||||
<th>architecture</th>
|
||||
<th>timestamp</th>
|
||||
<th>status</th>
|
||||
</tr>
|
||||
{% include "search-line.jinja2" %}
|
||||
|
||||
{% for package in packages %}
|
||||
<tr>
|
||||
<td class="package"><a href="{{ package.web_url|e }}" title="{{ package.base|e }}">{{ package.base|e }}</a></td>
|
||||
<td>{{ package.packages|join("<br>"|safe) }}</td>
|
||||
<td>{{ package.version|e }}</td>
|
||||
<td>{{ architecture|e }}</td>
|
||||
<td>{{ package.timestamp|e }}</td>
|
||||
<td class="package-{{ package.status|e }}">{{ package.status|e }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<section class="element">
|
||||
<table class="sortable search-table">
|
||||
<tr class="header">
|
||||
<th>package base</th>
|
||||
<th>packages</th>
|
||||
<th>version</th>
|
||||
<th>last update</th>
|
||||
<th>status</th>
|
||||
</tr>
|
||||
|
||||
{% for package in packages %}
|
||||
<tr class="package">
|
||||
<td class="include-search"><a href="{{ package.web_url|e }}" title="{{ package.base|e }}">{{ package.base|e }}</a></td>
|
||||
<td class="include-search">{{ package.packages|join("<br>"|safe) }}</td>
|
||||
<td>{{ package.version|e }}</td>
|
||||
<td>{{ package.timestamp|e }}</td>
|
||||
<td class="status package-{{ package.status|e }}">{{ package.status|e }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<ul class="navigation">
|
||||
<li><a href="https://github.com/arcan1s/ahriman" title="sources">ahriman</a></li>
|
||||
<li><a href="https://github.com/arcan1s/ahriman/releases" title="releases list">releases</a></li>
|
||||
<li><a href="https://github.com/arcan1s/ahriman/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
@ -2,31 +2,61 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ repository|e }}</title>
|
||||
|
||||
{% include "style.jinja2" %}
|
||||
|
||||
{% include "sorttable.jinja2" %}
|
||||
{% include "search.jinja2" %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Archlinux custom repository</h1>
|
||||
<div class="root">
|
||||
<h1>Archlinux user repository</h1>
|
||||
|
||||
{% if pgp_key is not none %}
|
||||
<p>This repository is signed with <a href="http://keys.gnupg.net/pks/lookup?search=0x{{ pgp_key|e }}" title="key search">{{ pgp_key|e }}</a>.</p>
|
||||
{% endif %}
|
||||
<section class="element">
|
||||
{% if pgp_key is not none %}
|
||||
<p>This repository is signed with <a href="http://keys.gnupg.net/pks/lookup?search=0x{{ pgp_key|e }}&fingerprint=on&op=index" title="key search">{{ pgp_key|e }}</a> by default.</p>
|
||||
{% endif %}
|
||||
|
||||
<code>
|
||||
$ cat /etc/pacman.conf<br>
|
||||
[{{ repository|e }}]<br>
|
||||
Server = {{ link_path|e }}<br>
|
||||
SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly
|
||||
</code>
|
||||
<code>
|
||||
$ cat /etc/pacman.conf<br>
|
||||
[{{ repository|e }}]<br>
|
||||
Server = {{ link_path|e }}<br>
|
||||
SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly
|
||||
</code>
|
||||
</section>
|
||||
|
||||
<p>Packages:</p>
|
||||
<ul>
|
||||
{% for package, package_url in packages.items() %}
|
||||
<li><a href="{{ package_url|e }}" title="{{ package|e }}">{{ package|e }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% include "search-line.jinja2" %}
|
||||
|
||||
{% if homepage is not none %}
|
||||
<footer><a href="{{ homepage|e }}" title="homepage">Homepage</a></footer>
|
||||
{% endif %}
|
||||
<section class="element">
|
||||
<table class="sortable search-table">
|
||||
<tr class="header">
|
||||
<th>package</th>
|
||||
<th>version</th>
|
||||
<th>archive size</th>
|
||||
<th>installed size</th>
|
||||
<th>build date</th>
|
||||
</tr>
|
||||
|
||||
{% for package in packages %}
|
||||
<tr class="package">
|
||||
<td class="include-search"><a href="{{ link_path|e }}/{{ package.filename|e }}" title="{{ package.name|e }}">{{ package.name|e }}</a></td>
|
||||
<td>{{ package.version|e }}</td>
|
||||
<td>{{ package.archive_size|e }}</td>
|
||||
<td>{{ package.installed_size|e }}</td>
|
||||
<td>{{ package.build_date|e }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<ul class="navigation">
|
||||
{% if homepage is not none %}
|
||||
<li><a href="{{ homepage|e }}" title="homepage">Homepage</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
3
package/share/ahriman/search-line.jinja2
Normal file
3
package/share/ahriman/search-line.jinja2
Normal file
@ -0,0 +1,3 @@
|
||||
<section class="element">
|
||||
<input type="search" id="search" onkeyup="searchInTable()" placeholder="search for package" title="search for package"/>
|
||||
</section>
|
25
package/share/ahriman/search.jinja2
Normal file
25
package/share/ahriman/search.jinja2
Normal file
@ -0,0 +1,25 @@
|
||||
<script type="text/javascript">
|
||||
function searchInTable() {
|
||||
const input = document.getElementById("search");
|
||||
const filter = input.value.toLowerCase();
|
||||
const tables = document.getElementsByClassName("search-table");
|
||||
|
||||
for (let i = 0; i < tables.length; i++) {
|
||||
const tr = tables[i].getElementsByTagName("tr");
|
||||
// from 1 coz of header
|
||||
for (let i = 1; i < tr.length; i++) {
|
||||
let td = tr[i].getElementsByClassName("include-search");
|
||||
let display = "none";
|
||||
for (let j = 0; j < td.length; j++) {
|
||||
if (td[j].tagName.toLowerCase() === "td") {
|
||||
if (td[j].innerHTML.toLowerCase().indexOf(filter) > -1) {
|
||||
display = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
tr[i].style.display = display;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
1
package/share/ahriman/sorttable.jinja2
Normal file
1
package/share/ahriman/sorttable.jinja2
Normal file
@ -0,0 +1 @@
|
||||
<script src="https://www.kryogenix.org/code/browser/sorttable/sorttable.js"></script>
|
136
package/share/ahriman/style.jinja2
Normal file
136
package/share/ahriman/style.jinja2
Normal file
@ -0,0 +1,136 @@
|
||||
<style>
|
||||
:root {
|
||||
--color-building: 255, 255, 146;
|
||||
--color-failed: 255, 94, 94;
|
||||
--color-pending: 255, 255, 146;
|
||||
--color-success: 94, 255, 94;
|
||||
--color-unknown: 225, 225, 225;
|
||||
|
||||
--color-header: 200, 200, 255;
|
||||
--color-hover: 255, 255, 225;
|
||||
--color-line-blue: 235, 235, 255;
|
||||
--color-line-white: 255, 255, 255;
|
||||
}
|
||||
|
||||
@keyframes blink-building {
|
||||
0% { background-color: rgba(var(--color-building), 1.0); }
|
||||
10% { background-color: rgba(var(--color-building), 0.9); }
|
||||
20% { background-color: rgba(var(--color-building), 0.8); }
|
||||
30% { background-color: rgba(var(--color-building), 0.7); }
|
||||
40% { background-color: rgba(var(--color-building), 0.6); }
|
||||
50% { background-color: rgba(var(--color-building), 0.5); }
|
||||
60% { background-color: rgba(var(--color-building), 0.4); }
|
||||
70% { background-color: rgba(var(--color-building), 0.3); }
|
||||
80% { background-color: rgba(var(--color-building), 0.2); }
|
||||
90% { background-color: rgba(var(--color-building), 0.1); }
|
||||
100% { background-color: rgba(var(--color-building), 0.0); }
|
||||
}
|
||||
|
||||
div.root {
|
||||
width: 70%;
|
||||
padding: 15px 15% 0;
|
||||
}
|
||||
|
||||
section.element, footer {
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
code, input, table {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
tr.package:nth-child(odd) {
|
||||
background-color: rgba(var(--color-line-white), 1.0);
|
||||
}
|
||||
|
||||
tr.package:nth-child(even) {
|
||||
background-color: rgba(var(--color-line-blue), 1.0);
|
||||
}
|
||||
|
||||
tr.package:hover {
|
||||
background-color: rgba(var(--color-hover), 1.0);
|
||||
}
|
||||
|
||||
tr.header{
|
||||
background-color: rgba(var(--color-header), 1.0);
|
||||
}
|
||||
|
||||
td.status {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td.package-unknown {
|
||||
background-color: rgba(var(--color-unknown), 1.0);
|
||||
}
|
||||
td.package-pending {
|
||||
background-color: rgba(var(--color-pending), 1.0);
|
||||
}
|
||||
td.package-building {
|
||||
background-color: rgba(var(--color-building), 1.0);
|
||||
animation-name: blink-building;
|
||||
animation-duration: 1s;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
td.package-failed {
|
||||
background-color: rgba(var(--color-failed), 1.0);
|
||||
}
|
||||
td.package-success {
|
||||
background-color: rgba(var(--color-success), 1.0);
|
||||
}
|
||||
|
||||
li.service-unknown {
|
||||
background-color: rgba(var(--color-unknown), 1.0);
|
||||
}
|
||||
li.service-building {
|
||||
background-color: rgba(var(--color-building), 1.0);
|
||||
animation-name: blink-building;
|
||||
animation-duration: 1s;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
li.service-failed {
|
||||
background-color: rgba(var(--color-failed), 1.0);
|
||||
}
|
||||
li.service-success {
|
||||
background-color: rgba(var(--color-success), 1.0);
|
||||
}
|
||||
|
||||
ul.navigation {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: rgba(var(--color-header), 1.0);
|
||||
}
|
||||
|
||||
ul.navigation li {
|
||||
float: left;
|
||||
}
|
||||
|
||||
ul.navigation li.status {
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
ul.navigation li a {
|
||||
display: block;
|
||||
color: black;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
ul.navigation li a:hover {
|
||||
background-color: rgba(var(--color-hover), 1.0);
|
||||
}
|
||||
</style>
|
5
setup.cfg
Normal file
5
setup.cfg
Normal file
@ -0,0 +1,5 @@
|
||||
[aliases]
|
||||
test = pytest
|
||||
|
||||
[tool:pytest]
|
||||
addopts = --cov=ahriman --pspec
|
87
setup.py
87
setup.py
@ -4,65 +4,92 @@ from os import path
|
||||
|
||||
here = path.abspath(path.dirname(__file__))
|
||||
metadata = dict()
|
||||
with open(convert_path('src/ahriman/version.py')) as metadata_file:
|
||||
with open(convert_path("src/ahriman/version.py")) as metadata_file:
|
||||
exec(metadata_file.read(), metadata)
|
||||
|
||||
setup(
|
||||
name='ahriman',
|
||||
name="ahriman",
|
||||
|
||||
version=metadata['__version__'],
|
||||
version=metadata["__version__"],
|
||||
zip_safe=False,
|
||||
|
||||
description='ArcHlinux ReposItory MANager',
|
||||
description="ArcHlinux ReposItory MANager",
|
||||
|
||||
author='arcanis',
|
||||
author_email='',
|
||||
url='https://github.com/arcan1s/ahriman',
|
||||
author="arcanis",
|
||||
author_email="",
|
||||
url="https://github.com/arcan1s/ahriman",
|
||||
|
||||
license='GPL3',
|
||||
license="GPL3",
|
||||
|
||||
packages=find_packages('src'),
|
||||
package_dir={'': 'src'},
|
||||
packages=find_packages("src"),
|
||||
package_dir={"": "src"},
|
||||
|
||||
dependency_links=[
|
||||
],
|
||||
install_requires=[
|
||||
'aur',
|
||||
'pyalpm',
|
||||
'srcinfo',
|
||||
"aur",
|
||||
"pyalpm",
|
||||
"srcinfo",
|
||||
],
|
||||
setup_requires=[
|
||||
'pytest-runner',
|
||||
"pytest-runner",
|
||||
],
|
||||
tests_require=[
|
||||
'pytest',
|
||||
"pytest",
|
||||
"pytest-aiohttp",
|
||||
"pytest-cov",
|
||||
"pytest-helpers-namespace",
|
||||
"pytest-mock",
|
||||
"pytest-pspec",
|
||||
"pytest-resource-path",
|
||||
],
|
||||
|
||||
include_package_data=True,
|
||||
scripts=[
|
||||
'package/bin/ahriman',
|
||||
"package/bin/ahriman",
|
||||
],
|
||||
data_files=[
|
||||
('/etc', [
|
||||
'package/etc/ahriman.ini',
|
||||
("/etc", [
|
||||
"package/etc/ahriman.ini",
|
||||
]),
|
||||
('/etc/ahriman.ini.d', [
|
||||
'package/etc/ahriman.ini.d/logging.ini',
|
||||
("/etc/ahriman.ini.d", [
|
||||
"package/etc/ahriman.ini.d/logging.ini",
|
||||
]),
|
||||
('lib/systemd/system', [
|
||||
'package/lib/systemd/system/ahriman@.service',
|
||||
'package/lib/systemd/system/ahriman@.timer',
|
||||
'package/lib/systemd/system/ahriman-web@.service',
|
||||
("lib/systemd/system", [
|
||||
"package/lib/systemd/system/ahriman@.service",
|
||||
"package/lib/systemd/system/ahriman@.timer",
|
||||
"package/lib/systemd/system/ahriman-web@.service",
|
||||
]),
|
||||
('share/ahriman', [
|
||||
'package/share/ahriman/build-status.jinja2',
|
||||
'package/share/ahriman/repo-index.jinja2',
|
||||
("share/ahriman", [
|
||||
"package/share/ahriman/build-status.jinja2",
|
||||
"package/share/ahriman/repo-index.jinja2",
|
||||
"package/share/ahriman/search.jinja2",
|
||||
"package/share/ahriman/search-line.jinja2",
|
||||
"package/share/ahriman/sorttable.jinja2",
|
||||
"package/share/ahriman/style.jinja2",
|
||||
]),
|
||||
],
|
||||
|
||||
extras_require={
|
||||
'html-templates': ['Jinja2'],
|
||||
'test': ['coverage', 'pytest'],
|
||||
'web': ['Jinja2', 'aiohttp', 'aiohttp_jinja2', 'requests'],
|
||||
"check": [
|
||||
"autopep8",
|
||||
"mypy",
|
||||
"pylint",
|
||||
],
|
||||
"test": [
|
||||
"pytest",
|
||||
"pytest-aiohttp",
|
||||
"pytest-cov",
|
||||
"pytest-helpers-namespace",
|
||||
"pytest-mock",
|
||||
"pytest-pspec",
|
||||
"pytest-resource-path",
|
||||
],
|
||||
"web": [
|
||||
"Jinja2",
|
||||
"aiohttp",
|
||||
"aiohttp_jinja2",
|
||||
"requests",
|
||||
],
|
||||
},
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -18,98 +18,106 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
import ahriman.application.handlers as handlers
|
||||
import ahriman.version as version
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.lock import Lock
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
# pylint: disable=too-many-statements
|
||||
def _parser() -> argparse.ArgumentParser:
|
||||
"""
|
||||
command line parser generator
|
||||
:return: command line parser for the application
|
||||
"""
|
||||
parser = argparse.ArgumentParser(prog="ahriman", description="ArcHlinux ReposItory MANager")
|
||||
parser.add_argument("-a", "--architecture", help="target architectures (can be used multiple times)",
|
||||
action="append", required=True)
|
||||
parser.add_argument("-c", "--config", help="configuration path", default="/etc/ahriman.ini")
|
||||
parser.add_argument("--force", help="force run, remove file lock", action="store_true")
|
||||
parser.add_argument("--lock", help="lock file", default="/tmp/ahriman.lock")
|
||||
parser.add_argument("--no-log", help="redirect all log messages to stderr", action="store_true")
|
||||
parser.add_argument("--no-report", help="force disable reporting to web service", action="store_true")
|
||||
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user", action="store_true")
|
||||
parser.add_argument("-v", "--version", action="version", version=version.__version__)
|
||||
|
||||
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True)
|
||||
|
||||
add_parser = subparsers.add_parser("add", description="add package")
|
||||
add_parser.add_argument("package", help="package base/name or archive path", nargs="+")
|
||||
add_parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true")
|
||||
add_parser.set_defaults(handler=handlers.Add)
|
||||
|
||||
check_parser = subparsers.add_parser("check", description="check for updates. Same as update --dry-run --no-manual")
|
||||
check_parser.add_argument("package", help="filter check by package base", nargs="*")
|
||||
check_parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
|
||||
check_parser.set_defaults(handler=handlers.Update, no_aur=False, no_manual=True, dry_run=True)
|
||||
|
||||
clean_parser = subparsers.add_parser("clean", description="clear all local caches")
|
||||
clean_parser.add_argument("--no-build", help="do not clear directory with package sources", action="store_true")
|
||||
clean_parser.add_argument("--no-cache", help="do not clear directory with package caches", action="store_true")
|
||||
clean_parser.add_argument("--no-chroot", help="do not clear build chroot", action="store_true")
|
||||
clean_parser.add_argument("--no-manual", help="do not clear directory with manually added packages",
|
||||
action="store_true")
|
||||
clean_parser.add_argument("--no-packages", help="do not clear directory with built packages", action="store_true")
|
||||
clean_parser.set_defaults(handler=handlers.Clean, unsafe=True)
|
||||
|
||||
config_parser = subparsers.add_parser("config", description="dump configuration for specified architecture")
|
||||
config_parser.set_defaults(handler=handlers.Dump, lock=None, no_report=True, unsafe=True)
|
||||
|
||||
rebuild_parser = subparsers.add_parser("rebuild", description="rebuild whole repository")
|
||||
rebuild_parser.set_defaults(handler=handlers.Rebuild)
|
||||
|
||||
remove_parser = subparsers.add_parser("remove", description="remove package")
|
||||
remove_parser.add_argument("package", help="package name or base", nargs="+")
|
||||
remove_parser.set_defaults(handler=handlers.Remove)
|
||||
|
||||
report_parser = subparsers.add_parser("report", description="generate report")
|
||||
report_parser.add_argument("target", help="target to generate report", nargs="*")
|
||||
report_parser.set_defaults(handler=handlers.Report)
|
||||
|
||||
setup_parser = subparsers.add_parser("setup", description="create initial service configuration, requires root")
|
||||
setup_parser.add_argument("--build-command", help="build command prefix", default="ahriman")
|
||||
setup_parser.add_argument("--from-config", help="path to default devtools pacman configuration",
|
||||
default="/usr/share/devtools/pacman-extra.conf")
|
||||
setup_parser.add_argument("--no-multilib", help="do not add multilib repository", action="store_true")
|
||||
setup_parser.add_argument("--packager", help="packager name and email", required=True)
|
||||
setup_parser.add_argument("--repository", help="repository name", default="aur-clone")
|
||||
setup_parser.set_defaults(handler=handlers.Setup, lock=None, no_report=True, unsafe=True)
|
||||
|
||||
sign_parser = subparsers.add_parser("sign", description="(re-)sign packages and repository database")
|
||||
sign_parser.add_argument("package", help="sign only specified packages", nargs="*")
|
||||
sign_parser.set_defaults(handler=handlers.Sign)
|
||||
|
||||
status_parser = subparsers.add_parser("status", description="request status of the package")
|
||||
status_parser.add_argument("--ahriman", help="get service status itself", action="store_true")
|
||||
status_parser.add_argument("package", help="filter status by package base", nargs="*")
|
||||
status_parser.set_defaults(handler=handlers.Status, lock=None, no_report=True, unsafe=True)
|
||||
|
||||
sync_parser = subparsers.add_parser("sync", description="sync packages to remote server")
|
||||
sync_parser.add_argument("target", help="target to sync", nargs="*")
|
||||
sync_parser.set_defaults(handler=handlers.Sync)
|
||||
|
||||
update_parser = subparsers.add_parser("update", description="run updates")
|
||||
update_parser.add_argument("package", help="filter check by package base", nargs="*")
|
||||
update_parser.add_argument("--dry-run", help="just perform check for updates, same as check command",
|
||||
action="store_true")
|
||||
update_parser.add_argument("--no-aur", help="do not check for AUR updates. Implies --no-vcs", action="store_true")
|
||||
update_parser.add_argument("--no-manual", help="do not include manual updates", action="store_true")
|
||||
update_parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
|
||||
update_parser.set_defaults(handler=handlers.Update)
|
||||
|
||||
web_parser = subparsers.add_parser("web", description="start web server")
|
||||
web_parser.set_defaults(handler=handlers.Web, lock=None, no_report=True)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def add(args: argparse.Namespace) -> None:
|
||||
Application.from_args(args).add(args.package, args.without_dependencies)
|
||||
if __name__ == "__main__":
|
||||
args_parser = _parser()
|
||||
args = args_parser.parse_args()
|
||||
|
||||
handler: handlers.Handler = args.handler
|
||||
status = handler.execute(args)
|
||||
|
||||
def rebuild(args: argparse.Namespace) -> None:
|
||||
app = Application.from_args(args)
|
||||
packages = app.repository.packages()
|
||||
app.update(packages)
|
||||
|
||||
|
||||
def remove(args: argparse.Namespace) -> None:
|
||||
Application.from_args(args).remove(args.package)
|
||||
|
||||
|
||||
def report(args: argparse.Namespace) -> None:
|
||||
Application.from_args(args).report(args.target)
|
||||
|
||||
|
||||
def sync(args: argparse.Namespace) -> None:
|
||||
Application.from_args(args).sync(args.target)
|
||||
|
||||
|
||||
def update(args: argparse.Namespace) -> None:
|
||||
app = Application.from_args(args)
|
||||
log_fn = lambda line: print(line) if args.dry_run else app.logger.info(line)
|
||||
packages = app.get_updates(args.no_aur, args.no_manual, args.no_vcs, log_fn)
|
||||
if args.dry_run:
|
||||
return
|
||||
app.update(packages)
|
||||
|
||||
|
||||
def web(args: argparse.Namespace) -> None:
|
||||
from ahriman.web.web import run_server, setup_service
|
||||
config = Configuration.from_path(args.config)
|
||||
app = setup_service(args.architecture, config)
|
||||
run_server(app, args.architecture)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(prog='ahriman', description='ArcHlinux ReposItory MANager')
|
||||
parser.add_argument('-a', '--architecture', help='target architecture', required=True)
|
||||
parser.add_argument('-c', '--config', help='configuration path', default='/etc/ahriman.ini')
|
||||
parser.add_argument('--force', help='force run, remove file lock', action='store_true')
|
||||
parser.add_argument('--lock', help='lock file', default='/tmp/ahriman.lock')
|
||||
parser.add_argument('-v', '--version', action='version', version=version.__version__)
|
||||
subparsers = parser.add_subparsers(title='command')
|
||||
|
||||
add_parser = subparsers.add_parser('add', description='add package')
|
||||
add_parser.add_argument('package', help='package name or archive path', nargs='+')
|
||||
add_parser.add_argument('--without-dependencies', help='do not add dependencies', action='store_true')
|
||||
add_parser.set_defaults(fn=add)
|
||||
|
||||
check_parser = subparsers.add_parser('check', description='check for updates')
|
||||
check_parser.set_defaults(fn=update, no_aur=False, no_manual=True, no_vcs=False, dry_run=True)
|
||||
|
||||
rebuild_parser = subparsers.add_parser('rebuild', description='rebuild whole repository')
|
||||
rebuild_parser.set_defaults(fn=rebuild)
|
||||
|
||||
remove_parser = subparsers.add_parser('remove', description='remove package')
|
||||
remove_parser.add_argument('package', help='package name', nargs='+')
|
||||
remove_parser.set_defaults(fn=remove)
|
||||
|
||||
report_parser = subparsers.add_parser('report', description='generate report')
|
||||
report_parser.add_argument('target', help='target to generate report', nargs='*')
|
||||
report_parser.set_defaults(fn=report)
|
||||
|
||||
sync_parser = subparsers.add_parser('sync', description='sync packages to remote server')
|
||||
sync_parser.add_argument('target', help='target to sync', nargs='*')
|
||||
sync_parser.set_defaults(fn=sync)
|
||||
|
||||
update_parser = subparsers.add_parser('update', description='run updates')
|
||||
update_parser.add_argument('--dry-run', help='just perform check for updates, same as check command', action='store_true')
|
||||
update_parser.add_argument('--no-aur', help='do not check for AUR updates', action='store_true')
|
||||
update_parser.add_argument('--no-manual', help='do not include manual updates', action='store_true')
|
||||
update_parser.add_argument('--no-vcs', help='do not check VCS packages', action='store_true')
|
||||
update_parser.set_defaults(fn=update)
|
||||
|
||||
web_parser = subparsers.add_parser('web', description='start web server')
|
||||
web_parser.set_defaults(fn=web, lock=None)
|
||||
|
||||
args = parser.parse_args()
|
||||
if 'fn' not in args:
|
||||
parser.print_help()
|
||||
exit(1)
|
||||
|
||||
with Lock(args.lock, args.force):
|
||||
args.fn(args)
|
||||
sys.exit(status)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -17,104 +17,195 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from typing import Callable, Iterable, List, Optional, Set, Type
|
||||
from pathlib import Path
|
||||
from typing import Callable, Iterable, List, Set
|
||||
|
||||
from ahriman.core.build_tools.task import Task
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.core.repository.repository import Repository
|
||||
from ahriman.core.tree import Tree
|
||||
from ahriman.core.util import package_like
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class Application:
|
||||
"""
|
||||
base application class
|
||||
:ivar architecture: repository architecture
|
||||
:ivar config: configuration instance
|
||||
:ivar logger: application logger
|
||||
:ivar repository: repository instance
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
||||
self.logger = logging.getLogger('root')
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
self.logger = logging.getLogger("root")
|
||||
self.config = config
|
||||
self.architecture = architecture
|
||||
self.repository = Repository(architecture, config)
|
||||
|
||||
@classmethod
|
||||
def from_args(cls: Type[Application], args: argparse.Namespace) -> Application:
|
||||
config = Configuration.from_path(args.config)
|
||||
return cls(args.architecture, config)
|
||||
|
||||
def _known_packages(self) -> Set[str]:
|
||||
known_packages = set()
|
||||
"""
|
||||
load packages from repository and pacman repositories
|
||||
:return: list of known packages
|
||||
"""
|
||||
known_packages: Set[str] = set()
|
||||
# local set
|
||||
for package in self.repository.packages():
|
||||
known_packages.update(package.packages)
|
||||
known_packages.update(package.packages.keys())
|
||||
known_packages.update(self.repository.pacman.all_packages())
|
||||
return known_packages
|
||||
|
||||
def _finalize(self) -> None:
|
||||
self.report()
|
||||
self.sync()
|
||||
"""
|
||||
generate report and sync to remote server
|
||||
"""
|
||||
self.report([])
|
||||
self.sync([])
|
||||
|
||||
def get_updates(self, no_aur: bool, no_manual: bool, no_vcs: bool,
|
||||
def get_updates(self, filter_packages: List[str], no_aur: bool, no_manual: bool, no_vcs: bool,
|
||||
log_fn: Callable[[str], None]) -> List[Package]:
|
||||
"""
|
||||
get list of packages to run update process
|
||||
:param filter_packages: do not check every package just specified in the list
|
||||
:param no_aur: do not check for aur updates
|
||||
:param no_manual: do not check for manual updates
|
||||
:param no_vcs: do not check VCS packages
|
||||
:param log_fn: logger function to log updates
|
||||
:return: list of out-of-dated packages
|
||||
"""
|
||||
updates = []
|
||||
|
||||
if not no_aur:
|
||||
updates.extend(self.repository.updates_aur(no_vcs))
|
||||
updates.extend(self.repository.updates_aur(filter_packages, no_vcs))
|
||||
if not no_manual:
|
||||
updates.extend(self.repository.updates_manual())
|
||||
|
||||
for package in updates:
|
||||
log_fn(f'{package.base} = {package.version}')
|
||||
log_fn(f"{package.base} = {package.version}")
|
||||
|
||||
return updates
|
||||
|
||||
def add(self, names: Iterable[str], without_dependencies: bool) -> None:
|
||||
"""
|
||||
add packages for the next build
|
||||
:param names: list of package bases to add
|
||||
:param without_dependencies: if set, dependency check will be disabled
|
||||
"""
|
||||
known_packages = self._known_packages()
|
||||
|
||||
def add_manual(name: str) -> str:
|
||||
package = Package.load(name, self.repository.pacman, self.config.get('alpm', 'aur_url'))
|
||||
path = os.path.join(self.repository.paths.manual, package.base)
|
||||
def add_directory(path: Path) -> None:
|
||||
for full_path in filter(package_like, path.iterdir()):
|
||||
add_archive(full_path)
|
||||
|
||||
def add_manual(src: str) -> Path:
|
||||
package = Package.load(src, self.repository.pacman, self.config.get("alpm", "aur_url"))
|
||||
path = self.repository.paths.manual / package.base
|
||||
Task.fetch(path, package.git_url)
|
||||
return path
|
||||
|
||||
def add_archive(src: str) -> None:
|
||||
dst = os.path.join(self.repository.paths.packages, os.path.basename(src))
|
||||
def add_archive(src: Path) -> None:
|
||||
dst = self.repository.paths.packages / src.name
|
||||
shutil.move(src, dst)
|
||||
|
||||
def process_dependencies(path: str) -> None:
|
||||
def process_dependencies(path: Path) -> None:
|
||||
if without_dependencies:
|
||||
return
|
||||
dependencies = Package.dependencies(path)
|
||||
self.add(dependencies.difference(known_packages), without_dependencies)
|
||||
|
||||
def process_single(name: str) -> None:
|
||||
if not os.path.isfile(name):
|
||||
path = add_manual(name)
|
||||
process_dependencies(path)
|
||||
def process_single(src: str) -> None:
|
||||
maybe_path = Path(src)
|
||||
if maybe_path.is_dir():
|
||||
add_directory(maybe_path)
|
||||
elif maybe_path.is_file():
|
||||
add_archive(maybe_path)
|
||||
else:
|
||||
add_archive(name)
|
||||
path = add_manual(src)
|
||||
process_dependencies(path)
|
||||
|
||||
for name in names:
|
||||
process_single(name)
|
||||
|
||||
def clean(self, no_build: bool, no_cache: bool, no_chroot: bool, no_manual: bool, no_packages: bool) -> None:
|
||||
"""
|
||||
run all clean methods. Warning: some functions might not be available under non-root
|
||||
:param no_build: do not clear directory with package sources
|
||||
:param no_cache: do not clear directory with package caches
|
||||
:param no_chroot: do not clear build chroot
|
||||
:param no_manual: do not clear directory with manually added packages
|
||||
:param no_packages: do not clear directory with built packages
|
||||
"""
|
||||
if not no_build:
|
||||
self.repository.clear_build()
|
||||
if not no_cache:
|
||||
self.repository.clear_cache()
|
||||
if not no_chroot:
|
||||
self.repository.clear_chroot()
|
||||
if not no_manual:
|
||||
self.repository.clear_manual()
|
||||
if not no_packages:
|
||||
self.repository.clear_packages()
|
||||
|
||||
def remove(self, names: Iterable[str]) -> None:
|
||||
"""
|
||||
remove packages from repository
|
||||
:param names: list of packages (either base or name) to remove
|
||||
"""
|
||||
self.repository.process_remove(names)
|
||||
self._finalize()
|
||||
|
||||
def report(self, target: Optional[Iterable[str]] = None) -> None:
|
||||
def report(self, target: Iterable[str]) -> None:
|
||||
"""
|
||||
generate report
|
||||
:param target: list of targets to run (e.g. html)
|
||||
"""
|
||||
targets = target or None
|
||||
self.repository.process_report(targets)
|
||||
|
||||
def sync(self, target: Optional[Iterable[str]] = None) -> None:
|
||||
def sign(self, packages: Iterable[str]) -> None:
|
||||
"""
|
||||
sign packages and repository
|
||||
:param packages: only sign specified packages
|
||||
"""
|
||||
# copy to prebuilt directory
|
||||
for package in self.repository.packages():
|
||||
# no one requested this package
|
||||
if packages and package.base not in packages:
|
||||
continue
|
||||
for archive in package.packages.values():
|
||||
if archive.filepath is None:
|
||||
continue # avoid mypy warning
|
||||
src = self.repository.paths.repository / archive.filepath
|
||||
dst = self.repository.paths.packages / archive.filepath
|
||||
shutil.copy(src, dst)
|
||||
# run generic update function
|
||||
self.update([])
|
||||
# sign repository database if set
|
||||
self.repository.sign.sign_repository(self.repository.repo.repo_path)
|
||||
self._finalize()
|
||||
|
||||
def sync(self, target: Iterable[str]) -> None:
|
||||
"""
|
||||
sync to remote server
|
||||
:param target: list of targets to run (e.g. s3)
|
||||
"""
|
||||
targets = target or None
|
||||
self.repository.process_sync(targets)
|
||||
|
||||
def update(self, updates: Iterable[Package]) -> None:
|
||||
def process_update(paths: Iterable[str]) -> None:
|
||||
"""
|
||||
run package updates
|
||||
:param updates: list of packages to update
|
||||
"""
|
||||
def process_update(paths: Iterable[Path]) -> None:
|
||||
self.repository.process_update(paths)
|
||||
self._finalize()
|
||||
|
||||
@ -123,9 +214,8 @@ class Application:
|
||||
process_update(packages)
|
||||
|
||||
# process manual packages
|
||||
tree = Tree()
|
||||
tree.load(updates)
|
||||
tree = Tree.load(updates)
|
||||
for num, level in enumerate(tree.levels()):
|
||||
self.logger.info(f'processing level #{num} {[package.base for package in level]}')
|
||||
self.logger.info(f"processing level #{num} {[package.base for package in level]}")
|
||||
packages = self.repository.process_build(level)
|
||||
process_update(packages)
|
||||
process_update(packages)
|
||||
|
33
src/ahriman/application/handlers/__init__.py
Normal file
33
src/ahriman/application/handlers/__init__.py
Normal file
@ -0,0 +1,33 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
|
||||
from ahriman.application.handlers.add import Add
|
||||
from ahriman.application.handlers.clean import Clean
|
||||
from ahriman.application.handlers.dump import Dump
|
||||
from ahriman.application.handlers.rebuild import Rebuild
|
||||
from ahriman.application.handlers.remove import Remove
|
||||
from ahriman.application.handlers.report import Report
|
||||
from ahriman.application.handlers.setup import Setup
|
||||
from ahriman.application.handlers.sign import Sign
|
||||
from ahriman.application.handlers.status import Status
|
||||
from ahriman.application.handlers.sync import Sync
|
||||
from ahriman.application.handlers.update import Update
|
||||
from ahriman.application.handlers.web import Web
|
42
src/ahriman/application/handlers/add.py
Normal file
42
src/ahriman/application/handlers/add.py
Normal file
@ -0,0 +1,42 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
class Add(Handler):
|
||||
"""
|
||||
add packages handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
Application(architecture, config).add(args.package, args.without_dependencies)
|
43
src/ahriman/application/handlers/clean.py
Normal file
43
src/ahriman/application/handlers/clean.py
Normal file
@ -0,0 +1,43 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
class Clean(Handler):
|
||||
"""
|
||||
clean caches handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
Application(architecture, config).clean(args.no_build, args.no_cache, args.no_chroot,
|
||||
args.no_manual, args.no_packages)
|
46
src/ahriman/application/handlers/dump.py
Normal file
46
src/ahriman/application/handlers/dump.py
Normal file
@ -0,0 +1,46 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
class Dump(Handler):
|
||||
"""
|
||||
dump config handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
config_dump = config.dump(architecture)
|
||||
for section, values in sorted(config_dump.items()):
|
||||
print(f"[{section}]")
|
||||
for key, value in sorted(values.items()):
|
||||
print(f"{key} = {value}")
|
||||
print()
|
75
src/ahriman/application/handlers/handler.py
Normal file
75
src/ahriman/application/handlers/handler.py
Normal file
@ -0,0 +1,75 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from multiprocessing import Pool
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.lock import Lock
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
class Handler:
|
||||
"""
|
||||
base handler class for command callbacks
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _call(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> bool:
|
||||
"""
|
||||
additional function to wrap all calls for multiprocessing library
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
:return: True on success, False otherwise
|
||||
"""
|
||||
try:
|
||||
with Lock(args, architecture, config):
|
||||
cls.run(args, architecture, config)
|
||||
return True
|
||||
except Exception:
|
||||
logging.getLogger("root").exception("process exception")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def execute(cls: Type[Handler], args: argparse.Namespace) -> int:
|
||||
"""
|
||||
execute function for all aru
|
||||
:param args: command line args
|
||||
:return: 0 on success, 1 otherwise
|
||||
"""
|
||||
configuration = Configuration.from_path(args.config, not args.no_log)
|
||||
with Pool(len(args.architecture)) as pool:
|
||||
result = pool.starmap(
|
||||
cls._call, [(args, architecture, configuration) for architecture in args.architecture])
|
||||
return 0 if all(result) else 1
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
raise NotImplementedError
|
44
src/ahriman/application/handlers/rebuild.py
Normal file
44
src/ahriman/application/handlers/rebuild.py
Normal file
@ -0,0 +1,44 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
class Rebuild(Handler):
|
||||
"""
|
||||
make world handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
application = Application(architecture, config)
|
||||
packages = application.repository.packages()
|
||||
application.update(packages)
|
42
src/ahriman/application/handlers/remove.py
Normal file
42
src/ahriman/application/handlers/remove.py
Normal file
@ -0,0 +1,42 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
class Remove(Handler):
|
||||
"""
|
||||
remove packages handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
Application(architecture, config).remove(args.package)
|
42
src/ahriman/application/handlers/report.py
Normal file
42
src/ahriman/application/handlers/report.py
Normal file
@ -0,0 +1,42 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
class Report(Handler):
|
||||
"""
|
||||
generate report handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
Application(architecture, config).report(args.target)
|
160
src/ahriman/application/handlers/setup.py
Normal file
160
src/ahriman/application/handlers/setup.py
Normal file
@ -0,0 +1,160 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
import configparser
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Setup(Handler):
|
||||
"""
|
||||
setup handler
|
||||
:cvar ARCHBUILD_COMMAND_PATH: default devtools command
|
||||
:cvar BIN_DIR_PATH: directory for custom binaries
|
||||
:cvar MIRRORLIST_PATH: path to pacman default mirrorlist (used by multilib repository)
|
||||
:cvar SUDOERS_PATH: path to sudoers.d include configuration
|
||||
"""
|
||||
|
||||
ARCHBUILD_COMMAND_PATH = Path("/usr/bin/archbuild")
|
||||
BIN_DIR_PATH = Path("/usr/local/bin")
|
||||
MIRRORLIST_PATH = Path("/etc/pacman.d/mirrorlist")
|
||||
SUDOERS_PATH = Path("/etc/sudoers.d/ahriman")
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
application = Application(architecture, config)
|
||||
Setup.create_makepkg_configuration(args.packager, application.repository.paths)
|
||||
Setup.create_executable(args.build_command, architecture)
|
||||
Setup.create_devtools_configuration(args.build_command, architecture, Path(args.from_config), args.no_multilib,
|
||||
args.repository, application.repository.paths)
|
||||
Setup.create_ahriman_configuration(args.build_command, architecture, args.repository, config.include)
|
||||
Setup.create_sudo_configuration(args.build_command, architecture)
|
||||
|
||||
@staticmethod
|
||||
def build_command(prefix: str, architecture: str) -> Path:
|
||||
"""
|
||||
generate build command name
|
||||
:param prefix: command prefix in {prefix}-{architecture}-build
|
||||
:param architecture: repository architecture
|
||||
:return: valid devtools command name
|
||||
"""
|
||||
return Setup.BIN_DIR_PATH / f"{prefix}-{architecture}-build"
|
||||
|
||||
@staticmethod
|
||||
def create_ahriman_configuration(prefix: str, architecture: str, repository: str, include_path: Path) -> None:
|
||||
"""
|
||||
create service specific configuration
|
||||
:param prefix: command prefix in {prefix}-{architecture}-build
|
||||
:param architecture: repository architecture
|
||||
:param repository: repository name
|
||||
:param include_path: path to directory with configuration includes
|
||||
"""
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
config.add_section("build")
|
||||
config.set("build", "build_command", str(Setup.build_command(prefix, architecture)))
|
||||
|
||||
config.add_section("repository")
|
||||
config.set("repository", "name", repository)
|
||||
|
||||
target = include_path / "build-overrides.ini"
|
||||
with target.open("w") as ahriman_config:
|
||||
config.write(ahriman_config)
|
||||
|
||||
@staticmethod
|
||||
def create_devtools_configuration(prefix: str, architecture: str, source: Path,
|
||||
no_multilib: bool, repository: str, paths: RepositoryPaths) -> None:
|
||||
"""
|
||||
create configuration for devtools based on `source` configuration
|
||||
:param prefix: command prefix in {prefix}-{architecture}-build
|
||||
:param architecture: repository architecture
|
||||
:param source: path to source configuration file
|
||||
:param no_multilib: do not add multilib repository
|
||||
:param repository: repository name
|
||||
:param paths: repository paths instance
|
||||
"""
|
||||
config = configparser.ConfigParser()
|
||||
# preserve case
|
||||
# stupid mypy thinks that it is impossible
|
||||
config.optionxform = lambda key: key # type: ignore
|
||||
|
||||
# load default configuration first
|
||||
# we cannot use Include here because it will be copied to new chroot, thus no includes there
|
||||
config.read(source)
|
||||
|
||||
# set our architecture now
|
||||
config.set("options", "Architecture", architecture)
|
||||
|
||||
# add multilib
|
||||
if not no_multilib:
|
||||
config.add_section("multilib")
|
||||
config.set("multilib", "Include", str(Setup.MIRRORLIST_PATH))
|
||||
|
||||
# add repository itself
|
||||
config.add_section(repository)
|
||||
config.set(repository, "SigLevel", "Optional TrustAll") # we don't care
|
||||
config.set(repository, "Server", f"file://{paths.repository}")
|
||||
|
||||
target = source.parent / f"pacman-{prefix}.conf"
|
||||
with target.open("w") as devtools_config:
|
||||
config.write(devtools_config)
|
||||
|
||||
@staticmethod
|
||||
def create_makepkg_configuration(packager: str, paths: RepositoryPaths) -> None:
|
||||
"""
|
||||
create configuration for makepkg
|
||||
:param packager: packager identifier (e.g. name, email)
|
||||
:param paths: repository paths instance
|
||||
"""
|
||||
(paths.root / ".makepkg.conf").write_text(f"PACKAGER='{packager}'\n")
|
||||
|
||||
@staticmethod
|
||||
def create_sudo_configuration(prefix: str, architecture: str) -> None:
|
||||
"""
|
||||
create configuration to run build command with sudo without password
|
||||
:param prefix: command prefix in {prefix}-{architecture}-build
|
||||
:param architecture: repository architecture
|
||||
"""
|
||||
command = Setup.build_command(prefix, architecture)
|
||||
Setup.SUDOERS_PATH.write_text(f"ahriman ALL=(ALL) NOPASSWD: {command} *\n")
|
||||
Setup.SUDOERS_PATH.chmod(0o400) # security!
|
||||
|
||||
@staticmethod
|
||||
def create_executable(prefix: str, architecture: str) -> None:
|
||||
"""
|
||||
create executable for the service
|
||||
:param prefix: command prefix in {prefix}-{architecture}-build
|
||||
:param architecture: repository architecture
|
||||
"""
|
||||
command = Setup.build_command(prefix, architecture)
|
||||
command.unlink(missing_ok=True)
|
||||
command.symlink_to(Setup.ARCHBUILD_COMMAND_PATH)
|
42
src/ahriman/application/handlers/sign.py
Normal file
42
src/ahriman/application/handlers/sign.py
Normal file
@ -0,0 +1,42 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
class Sign(Handler):
|
||||
"""
|
||||
(re-)sign handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
Application(architecture, config).sign(args.package)
|
58
src/ahriman/application/handlers/status.py
Normal file
58
src/ahriman/application/handlers/status.py
Normal file
@ -0,0 +1,58 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from typing import Iterable, Tuple, Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.build_status import BuildStatus
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class Status(Handler):
|
||||
"""
|
||||
package status handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
application = Application(architecture, config)
|
||||
if args.ahriman:
|
||||
ahriman = application.repository.reporter.get_self()
|
||||
print(ahriman.pretty_print())
|
||||
print()
|
||||
if args.package:
|
||||
packages: Iterable[Tuple[Package, BuildStatus]] = sum(
|
||||
[application.repository.reporter.get(base) for base in args.package],
|
||||
start=[])
|
||||
else:
|
||||
packages = application.repository.reporter.get(None)
|
||||
for package, package_status in sorted(packages, key=lambda item: item[0].base):
|
||||
print(package.pretty_print())
|
||||
print(f"\t{package.version}")
|
||||
print(f"\t{package_status.pretty_print()}")
|
42
src/ahriman/application/handlers/sync.py
Normal file
42
src/ahriman/application/handlers/sync.py
Normal file
@ -0,0 +1,42 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
class Sync(Handler):
|
||||
"""
|
||||
remove sync handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
Application(architecture, config).sync(args.target)
|
51
src/ahriman/application/handlers/update.py
Normal file
51
src/ahriman/application/handlers/update.py
Normal file
@ -0,0 +1,51 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
class Update(Handler):
|
||||
"""
|
||||
package update handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
# typing workaround
|
||||
def log_fn(line: str) -> None:
|
||||
return print(line) if args.dry_run else application.logger.info(line)
|
||||
|
||||
application = Application(architecture, config)
|
||||
packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn)
|
||||
if args.dry_run:
|
||||
return
|
||||
|
||||
application.update(packages)
|
43
src/ahriman/application/handlers/web.py
Normal file
43
src/ahriman/application/handlers/web.py
Normal file
@ -0,0 +1,43 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
class Web(Handler):
|
||||
"""
|
||||
web server handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
from ahriman.web.web import run_server, setup_service
|
||||
application = setup_service(architecture, config)
|
||||
run_server(application)
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -17,42 +17,100 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
from types import TracebackType
|
||||
from typing import Literal, Optional, Type
|
||||
|
||||
from ahriman.core.exceptions import DuplicateRun
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import DuplicateRun, UnsafeRun
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
|
||||
|
||||
class Lock:
|
||||
"""
|
||||
wrapper for application lock file
|
||||
:ivar force: remove lock file on start if any
|
||||
:ivar path: path to lock file if any
|
||||
:ivar reporter: build status reporter instance
|
||||
:ivar root: repository root (i.e. ahriman home)
|
||||
:ivar unsafe: skip user check
|
||||
"""
|
||||
|
||||
def __init__(self, path: Optional[str], force: bool) -> None:
|
||||
self.path = path
|
||||
self.force = force
|
||||
def __init__(self, args: argparse.Namespace, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
self.path = Path(f"{args.lock}_{architecture}") if args.lock is not None else None
|
||||
self.force = args.force
|
||||
self.unsafe = args.unsafe
|
||||
|
||||
def __enter__(self):
|
||||
if self.force:
|
||||
self.remove()
|
||||
self.check()
|
||||
self.root = Path(config.get("repository", "root"))
|
||||
self.reporter = Client() if args.no_report else Client.load(architecture, config)
|
||||
|
||||
def __enter__(self) -> Lock:
|
||||
"""
|
||||
default workflow is the following:
|
||||
|
||||
check user UID
|
||||
remove lock file if force flag is set
|
||||
check if there is lock file
|
||||
create lock file
|
||||
report to web if enabled
|
||||
"""
|
||||
self.check_user()
|
||||
self.create()
|
||||
self.reporter.update_self(BuildStatusEnum.Building)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
def __exit__(self, exc_type: Optional[Type[Exception]], exc_val: Optional[Exception],
|
||||
exc_tb: TracebackType) -> Literal[False]:
|
||||
"""
|
||||
remove lock file when done
|
||||
:param exc_type: exception type name if any
|
||||
:param exc_val: exception raised if any
|
||||
:param exc_tb: exception traceback if any
|
||||
:return: always False (do not suppress any exception)
|
||||
"""
|
||||
self.remove()
|
||||
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
|
||||
self.reporter.update_self(status)
|
||||
return False
|
||||
|
||||
def check(self) -> None:
|
||||
if self.path is None:
|
||||
def check_user(self) -> None:
|
||||
"""
|
||||
check if current user is actually owner of ahriman root
|
||||
"""
|
||||
if self.unsafe:
|
||||
return
|
||||
if os.path.exists(self.path):
|
||||
raise DuplicateRun()
|
||||
current_uid = os.getuid()
|
||||
root_uid = self.root.stat().st_uid
|
||||
if current_uid != root_uid:
|
||||
raise UnsafeRun(current_uid, root_uid)
|
||||
|
||||
def create(self) -> None:
|
||||
"""
|
||||
create lock file
|
||||
"""
|
||||
if self.path is None:
|
||||
return
|
||||
open(self.path, 'w').close()
|
||||
try:
|
||||
self.path.touch(exist_ok=self.force)
|
||||
except FileExistsError:
|
||||
raise DuplicateRun()
|
||||
|
||||
def remove(self) -> None:
|
||||
"""
|
||||
remove lock file
|
||||
"""
|
||||
if self.path is None:
|
||||
return
|
||||
if os.path.exists(self.path):
|
||||
os.remove(self.path)
|
||||
self.path.unlink(missing_ok=True)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -17,24 +17,36 @@
|
||||
# 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 pyalpm import Handle
|
||||
from pyalpm import Handle # type: ignore
|
||||
from typing import List, Set
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
class Pacman:
|
||||
"""
|
||||
alpm wrapper
|
||||
:ivar handle: pyalpm root `Handle`
|
||||
"""
|
||||
|
||||
def __init__(self, config: Configuration) -> None:
|
||||
root = config.get('alpm', 'root')
|
||||
pacman_root = config.get('alpm', 'database')
|
||||
self.handle = Handle(root, pacman_root)
|
||||
for repository in config.getlist('alpm', 'repositories'):
|
||||
"""
|
||||
default constructor
|
||||
:param config: configuration instance
|
||||
"""
|
||||
root = config.get("alpm", "root")
|
||||
pacman_root = config.getpath("alpm", "database")
|
||||
self.handle = Handle(root, str(pacman_root))
|
||||
for repository in config.getlist("alpm", "repositories"):
|
||||
self.handle.register_syncdb(repository, 0) # 0 is pgp_level
|
||||
|
||||
def all_packages(self) -> List[str]:
|
||||
"""
|
||||
get list of packages known for alpm
|
||||
:return: list of package names
|
||||
"""
|
||||
result: Set[str] = set()
|
||||
for database in self.handle.get_syncdbs():
|
||||
result.update({package.name for package in database.pkgcache})
|
||||
|
||||
return list(result)
|
||||
return list(result)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -18,8 +18,8 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
import os
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from ahriman.core.exceptions import BuildFailed
|
||||
@ -28,30 +28,59 @@ from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Repo:
|
||||
"""
|
||||
repo-add and repo-remove wrapper
|
||||
:ivar logger: class logger
|
||||
:ivar name: repository name
|
||||
:ivar paths: repository paths instance
|
||||
:ivar sign_args: additional args which have to be used to sign repository archive
|
||||
"""
|
||||
|
||||
_check_output = check_output
|
||||
|
||||
def __init__(self, name: str, paths: RepositoryPaths, sign_args: List[str]) -> None:
|
||||
self.logger = logging.getLogger('build_details')
|
||||
"""
|
||||
default constructor
|
||||
:param name: repository name
|
||||
:param paths: repository paths instance
|
||||
:param sign_args: additional args which have to be used to sign repository archive
|
||||
"""
|
||||
self.logger = logging.getLogger("build_details")
|
||||
self.name = name
|
||||
self.paths = paths
|
||||
self.sign_args = sign_args
|
||||
|
||||
@property
|
||||
def repo_path(self) -> str:
|
||||
return os.path.join(self.paths.repository, f'{self.name}.db.tar.gz')
|
||||
def repo_path(self) -> Path:
|
||||
"""
|
||||
:return: path to repository database
|
||||
"""
|
||||
return self.paths.repository / f"{self.name}.db.tar.gz"
|
||||
|
||||
def add(self, path: str) -> None:
|
||||
check_output(
|
||||
'repo-add', *self.sign_args, '-R', self.repo_path, path,
|
||||
exception=BuildFailed(path),
|
||||
def add(self, path: Path) -> None:
|
||||
"""
|
||||
add new package to repository
|
||||
:param path: path to archive to add
|
||||
"""
|
||||
Repo._check_output(
|
||||
"repo-add", *self.sign_args, "-R", str(self.repo_path), str(path),
|
||||
exception=BuildFailed(path.name),
|
||||
cwd=self.paths.repository,
|
||||
logger=self.logger)
|
||||
|
||||
def remove(self, prefix: str, package: str) -> None:
|
||||
for fn in filter(lambda f: f.startswith(prefix), os.listdir(self.paths.repository)):
|
||||
full_path = os.path.join(self.paths.repository, fn)
|
||||
os.remove(full_path)
|
||||
check_output(
|
||||
'repo-remove', *self.sign_args, self.repo_path, package,
|
||||
def remove(self, package: str, filename: Path) -> None:
|
||||
"""
|
||||
remove package from repository
|
||||
:param package: package name to remove
|
||||
:param filename: package filename to remove
|
||||
"""
|
||||
# remove package and signature (if any) from filesystem
|
||||
for full_path in self.paths.repository.glob(f"{filename}*"):
|
||||
full_path.unlink()
|
||||
|
||||
# remove package from registry
|
||||
Repo._check_output(
|
||||
"repo-remove", *self.sign_args, str(self.repo_path), package,
|
||||
exception=BuildFailed(package),
|
||||
cwd=self.paths.repository,
|
||||
logger=self.logger)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -17,10 +17,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import os
|
||||
import logging
|
||||
import shutil
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
@ -31,46 +31,98 @@ from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Task:
|
||||
"""
|
||||
base package build task
|
||||
:ivar build_logger: logger for build process
|
||||
:ivar logger: class logger
|
||||
:ivar package: package definitions
|
||||
:ivar paths: repository paths instance
|
||||
"""
|
||||
|
||||
_check_output = check_output
|
||||
|
||||
def __init__(self, package: Package, architecture: str, config: Configuration, paths: RepositoryPaths) -> None:
|
||||
self.logger = logging.getLogger('builder')
|
||||
self.build_logger = logging.getLogger('build_details')
|
||||
"""
|
||||
default constructor
|
||||
:param package: package definitions
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
:param paths: repository paths instance
|
||||
"""
|
||||
self.logger = logging.getLogger("builder")
|
||||
self.build_logger = logging.getLogger("build_details")
|
||||
self.package = package
|
||||
self.paths = paths
|
||||
|
||||
section = config.get_section_name('build', architecture)
|
||||
self.archbuild_flags = config.getlist(section, 'archbuild_flags')
|
||||
self.build_command = config.get(section, 'build_command')
|
||||
self.makepkg_flags = config.getlist(section, 'makepkg_flags')
|
||||
self.makechrootpkg_flags = config.getlist(section, 'makechrootpkg_flags')
|
||||
section = config.get_section_name("build", architecture)
|
||||
self.archbuild_flags = config.getlist(section, "archbuild_flags")
|
||||
self.build_command = config.get(section, "build_command")
|
||||
self.makepkg_flags = config.getlist(section, "makepkg_flags")
|
||||
self.makechrootpkg_flags = config.getlist(section, "makechrootpkg_flags")
|
||||
|
||||
@property
|
||||
def git_path(self) -> str:
|
||||
return os.path.join(self.paths.sources, self.package.base)
|
||||
def cache_path(self) -> Path:
|
||||
"""
|
||||
:return: path to cached packages
|
||||
"""
|
||||
return self.paths.cache / self.package.base
|
||||
|
||||
@property
|
||||
def git_path(self) -> Path:
|
||||
"""
|
||||
:return: path to clone package from git
|
||||
"""
|
||||
return self.paths.sources / self.package.base
|
||||
|
||||
@staticmethod
|
||||
def fetch(local: str, remote: str) -> None:
|
||||
shutil.rmtree(local, ignore_errors=True) # remove in case if file exists
|
||||
check_output('git', 'clone', remote, local, exception=None)
|
||||
def fetch(local: Path, remote: str, branch: str = "master") -> None:
|
||||
"""
|
||||
either clone repository or update it to origin/`branch`
|
||||
:param local: local path to fetch
|
||||
:param remote: remote target (from where to fetch)
|
||||
:param branch: branch name to checkout, master by default
|
||||
"""
|
||||
logger = logging.getLogger("build_details")
|
||||
# local directory exists and there is .git directory
|
||||
if (local / ".git").is_dir():
|
||||
Task._check_output("git", "fetch", "origin", branch, exception=None, cwd=local, logger=logger)
|
||||
else:
|
||||
Task._check_output("git", "clone", remote, str(local), exception=None, logger=logger)
|
||||
# and now force reset to our branch
|
||||
Task._check_output("git", "checkout", "--force", branch, exception=None, cwd=local, logger=logger)
|
||||
Task._check_output("git", "reset", "--hard", f"origin/{branch}", exception=None, cwd=local, logger=logger)
|
||||
|
||||
def build(self) -> List[str]:
|
||||
cmd = [self.build_command, '-r', self.paths.chroot]
|
||||
def build(self) -> List[Path]:
|
||||
"""
|
||||
run package build
|
||||
:return: paths of produced packages
|
||||
"""
|
||||
cmd = [self.build_command, "-r", str(self.paths.chroot)]
|
||||
cmd.extend(self.archbuild_flags)
|
||||
cmd.extend(['--'] + self.makechrootpkg_flags)
|
||||
cmd.extend(['--'] + self.makepkg_flags)
|
||||
self.logger.info(f'using {cmd} for {self.package.base}')
|
||||
cmd.extend(["--"] + self.makechrootpkg_flags)
|
||||
cmd.extend(["--"] + self.makepkg_flags)
|
||||
self.logger.info(f"using {cmd} for {self.package.base}")
|
||||
|
||||
check_output(
|
||||
Task._check_output(
|
||||
*cmd,
|
||||
exception=BuildFailed(self.package.base),
|
||||
cwd=self.git_path,
|
||||
logger=self.build_logger)
|
||||
|
||||
# well it is not actually correct, but we can deal with it
|
||||
return check_output('makepkg', '--packagelist',
|
||||
exception=BuildFailed(self.package.base),
|
||||
cwd=self.git_path).splitlines()
|
||||
packages = Task._check_output("makepkg", "--packagelist",
|
||||
exception=BuildFailed(self.package.base),
|
||||
cwd=self.git_path,
|
||||
logger=self.build_logger).splitlines()
|
||||
return [Path(package) for package in packages]
|
||||
|
||||
def clone(self, path: Optional[str] = None) -> None:
|
||||
def init(self, path: Optional[Path] = None) -> None:
|
||||
"""
|
||||
fetch package from git
|
||||
:param path: optional local path to fetch. If not set default path will be used
|
||||
"""
|
||||
git_path = path or self.git_path
|
||||
if self.cache_path.is_dir():
|
||||
# no need to clone whole repository, just copy from cache first
|
||||
shutil.copytree(self.cache_path, git_path)
|
||||
return Task.fetch(git_path, self.package.git_url)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -20,51 +20,146 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import os
|
||||
import logging
|
||||
|
||||
from logging.config import fileConfig
|
||||
from typing import List, Optional, Type
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Type
|
||||
|
||||
|
||||
# built-in configparser extension
|
||||
class Configuration(configparser.RawConfigParser):
|
||||
"""
|
||||
extension for built-in configuration parser
|
||||
:ivar path: path to root configuration file
|
||||
:cvar ARCHITECTURE_SPECIFIC_SECTIONS: known sections which can be architecture specific (required by dump)
|
||||
:cvar DEFAULT_LOG_FORMAT: default log format (in case of fallback)
|
||||
:cvar DEFAULT_LOG_LEVEL: default log level (in case of fallback)
|
||||
:cvar STATIC_SECTIONS: known sections which are not architecture specific (required by dump)
|
||||
"""
|
||||
|
||||
DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s"
|
||||
DEFAULT_LOG_LEVEL = logging.DEBUG
|
||||
|
||||
STATIC_SECTIONS = ["alpm", "report", "repository", "settings", "upload"]
|
||||
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "html", "rsync", "s3", "sign", "web"]
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
default constructor. In the most cases must not be called directly
|
||||
"""
|
||||
configparser.RawConfigParser.__init__(self, allow_no_value=True)
|
||||
self.path: Optional[str] = None
|
||||
self.path: Optional[Path] = None
|
||||
|
||||
@property
|
||||
def include(self) -> str:
|
||||
return self.get('settings', 'include')
|
||||
def include(self) -> Path:
|
||||
"""
|
||||
:return: path to directory with configuration includes
|
||||
"""
|
||||
return self.getpath("settings", "include")
|
||||
|
||||
@classmethod
|
||||
def from_path(cls: Type[Configuration], path: str) -> Configuration:
|
||||
def from_path(cls: Type[Configuration], path: Path, logfile: bool) -> Configuration:
|
||||
"""
|
||||
constructor with full object initialization
|
||||
:param path: path to root configuration file
|
||||
:param logfile: use log file to output messages
|
||||
:return: configuration instance
|
||||
"""
|
||||
config = cls()
|
||||
config.load(path)
|
||||
config.load_logging()
|
||||
config.load_logging(logfile)
|
||||
return config
|
||||
|
||||
def dump(self, architecture: str) -> Dict[str, Dict[str, str]]:
|
||||
"""
|
||||
dump configuration to dictionary
|
||||
:param architecture: repository architecture
|
||||
:return: configuration dump for specific architecture
|
||||
"""
|
||||
result: Dict[str, Dict[str, str]] = {}
|
||||
for section in Configuration.STATIC_SECTIONS:
|
||||
if not self.has_section(section):
|
||||
continue
|
||||
result[section] = dict(self[section])
|
||||
for group in Configuration.ARCHITECTURE_SPECIFIC_SECTIONS:
|
||||
section = self.get_section_name(group, architecture)
|
||||
if not self.has_section(section):
|
||||
continue
|
||||
result[section] = dict(self[section])
|
||||
|
||||
return result
|
||||
|
||||
def getlist(self, section: str, key: str) -> List[str]:
|
||||
"""
|
||||
get space separated string list option
|
||||
:param section: section name
|
||||
:param key: key name
|
||||
:return: list of string if option is set, empty list otherwise
|
||||
"""
|
||||
raw = self.get(section, key, fallback=None)
|
||||
if not raw: # empty string or none
|
||||
return []
|
||||
return raw.split()
|
||||
|
||||
def getpath(self, section: str, key: str) -> Path:
|
||||
"""
|
||||
helper to generate absolute configuration path for relative settings value
|
||||
:param section: section name
|
||||
:param key: key name
|
||||
:return: absolute path according to current path configuration
|
||||
"""
|
||||
value = Path(self.get(section, key))
|
||||
if self.path is None or value.is_absolute():
|
||||
return value
|
||||
return self.path.parent / value
|
||||
|
||||
def get_section_name(self, prefix: str, suffix: str) -> str:
|
||||
probe = f'{prefix}_{suffix}'
|
||||
"""
|
||||
check if there is `prefix`_`suffix` section and return it on success. Return `prefix` otherwise
|
||||
:param prefix: section name prefix
|
||||
:param suffix: section name suffix (e.g. architecture name)
|
||||
:return: found section name
|
||||
"""
|
||||
probe = f"{prefix}_{suffix}"
|
||||
return probe if self.has_section(probe) else prefix
|
||||
|
||||
def load(self, path: str) -> None:
|
||||
def load(self, path: Path) -> None:
|
||||
"""
|
||||
fully load configuration
|
||||
:param path: path to root configuration file
|
||||
"""
|
||||
self.path = path
|
||||
self.read(self.path)
|
||||
self.load_includes()
|
||||
|
||||
def load_includes(self) -> None:
|
||||
"""
|
||||
load configuration includes
|
||||
"""
|
||||
try:
|
||||
for conf in filter(lambda p: p.endswith('.ini'), sorted(os.listdir(self.include))):
|
||||
self.read(os.path.join(self.include, conf))
|
||||
for path in sorted(self.include.glob("*.ini")):
|
||||
self.read(path)
|
||||
except (FileNotFoundError, configparser.NoOptionError):
|
||||
pass
|
||||
|
||||
def load_logging(self) -> None:
|
||||
fileConfig(self.get('settings', 'logging'))
|
||||
def load_logging(self, logfile: bool) -> None:
|
||||
"""
|
||||
setup logging settings from configuration
|
||||
:param logfile: use log file to output messages
|
||||
"""
|
||||
def file_logger() -> None:
|
||||
try:
|
||||
config_path = self.getpath("settings", "logging")
|
||||
fileConfig(config_path)
|
||||
except (FileNotFoundError, PermissionError):
|
||||
console_logger()
|
||||
logging.exception("could not create logfile, fallback to stderr")
|
||||
|
||||
def console_logger() -> None:
|
||||
logging.basicConfig(filename=None, format=Configuration.DEFAULT_LOG_FORMAT,
|
||||
level=Configuration.DEFAULT_LOG_LEVEL)
|
||||
|
||||
if logfile:
|
||||
file_logger()
|
||||
else:
|
||||
console_logger()
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -21,35 +21,112 @@ from typing import Any
|
||||
|
||||
|
||||
class BuildFailed(Exception):
|
||||
"""
|
||||
base exception for failed builds
|
||||
"""
|
||||
|
||||
def __init__(self, package: str) -> None:
|
||||
Exception.__init__(self, f'Package {package} build failed, check logs for details')
|
||||
"""
|
||||
default constructor
|
||||
:param package: package base raised exception
|
||||
"""
|
||||
Exception.__init__(self, f"Package {package} build failed, check logs for details")
|
||||
|
||||
|
||||
class DuplicateRun(Exception):
|
||||
"""
|
||||
exception which will be raised if there is another application instance
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
Exception.__init__(self, 'Another application instance is run')
|
||||
"""
|
||||
default constructor
|
||||
"""
|
||||
Exception.__init__(self, "Another application instance is run")
|
||||
|
||||
|
||||
class InitializeException(Exception):
|
||||
"""
|
||||
base service initialization exception
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
Exception.__init__(self, 'Could not load service')
|
||||
"""
|
||||
default constructor
|
||||
"""
|
||||
Exception.__init__(self, "Could not load service")
|
||||
|
||||
|
||||
class InvalidOption(Exception):
|
||||
"""
|
||||
exception which will be raised on configuration errors
|
||||
"""
|
||||
|
||||
def __init__(self, value: Any) -> None:
|
||||
Exception.__init__(self, f'Invalid or unknown option value `{value}`')
|
||||
"""
|
||||
default constructor
|
||||
:param value: option value
|
||||
"""
|
||||
Exception.__init__(self, f"Invalid or unknown option value `{value}`")
|
||||
|
||||
|
||||
class InvalidPackageInfo(Exception):
|
||||
"""
|
||||
exception which will be raised on package load errors
|
||||
"""
|
||||
|
||||
def __init__(self, details: Any) -> None:
|
||||
Exception.__init__(self, f'There are errors during reading package information: `{details}`')
|
||||
"""
|
||||
default constructor
|
||||
:param details: error details
|
||||
"""
|
||||
Exception.__init__(self, f"There are errors during reading package information: `{details}`")
|
||||
|
||||
|
||||
class ReportFailed(Exception):
|
||||
"""
|
||||
report generation exception
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
Exception.__init__(self, 'Report failed')
|
||||
"""
|
||||
default constructor
|
||||
"""
|
||||
Exception.__init__(self, "Report failed")
|
||||
|
||||
|
||||
class SyncFailed(Exception):
|
||||
"""
|
||||
remote synchronization exception
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
Exception.__init__(self, 'Sync failed')
|
||||
"""
|
||||
default constructor
|
||||
"""
|
||||
Exception.__init__(self, "Sync failed")
|
||||
|
||||
|
||||
class UnknownPackage(Exception):
|
||||
"""
|
||||
exception for status watcher which will be thrown on unknown package
|
||||
"""
|
||||
|
||||
def __init__(self, base: str) -> None:
|
||||
Exception.__init__(self, f"Package base {base} is unknown")
|
||||
|
||||
|
||||
class UnsafeRun(Exception):
|
||||
"""
|
||||
exception which will be raised in case if user is not owner of repository
|
||||
"""
|
||||
|
||||
def __init__(self, current_uid: int, root_uid: int) -> None:
|
||||
"""
|
||||
default constructor
|
||||
"""
|
||||
Exception.__init__(
|
||||
self,
|
||||
f"""Current UID {current_uid} differs from root owner {root_uid}.
|
||||
Note that for the most actions it is unsafe to run application as different user.
|
||||
If you are 100% sure that it must be there try --unsafe option""")
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -18,53 +18,88 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import jinja2
|
||||
import os
|
||||
|
||||
from typing import Dict
|
||||
from typing import Callable, Dict, Iterable
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.report.report import Report
|
||||
from ahriman.core.util import package_like
|
||||
from ahriman.core.util import pretty_size, pretty_datetime
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.sign_settings import SignSettings
|
||||
|
||||
|
||||
class HTML(Report):
|
||||
"""
|
||||
html report generator
|
||||
|
||||
It uses jinja2 templates for report generation, the following variables are allowed:
|
||||
|
||||
homepage - link to homepage, string, optional
|
||||
link_path - prefix fo packages to download, string, required
|
||||
has_package_signed - True in case if package sign enabled, False otherwise, required
|
||||
has_repo_signed - True in case if repository database sign enabled, False otherwise, required
|
||||
packages - sorted list of packages properties: archive_size, build_date, filename, installed_size, name, version. Required
|
||||
pgp_key - default PGP key ID, string, optional
|
||||
repository - repository name, string, required
|
||||
|
||||
:ivar homepage: homepage link if any (for footer)
|
||||
:ivar link_path: prefix fo packages to download
|
||||
:ivar name: repository name
|
||||
:ivar pgp_key: default PGP key
|
||||
:ivar report_path: output path to html report
|
||||
:ivar sign_targets: targets to sign enabled in configuration
|
||||
:ivar tempate_path: path to directory with jinja templates
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
Report.__init__(self, architecture, config)
|
||||
section = config.get_section_name('html', architecture)
|
||||
self.report_path = config.get(section, 'path')
|
||||
|
||||
self.link_path = config.get(section, 'link_path')
|
||||
self.template_path = config.get(section, 'template_path')
|
||||
section = config.get_section_name("html", architecture)
|
||||
self.report_path = config.getpath(section, "path")
|
||||
self.link_path = config.get(section, "link_path")
|
||||
self.template_path = config.getpath(section, "template_path")
|
||||
|
||||
# base template vars
|
||||
self.sign_targets = [SignSettings.from_option(opt) for opt in config.getlist('sign', 'target')]
|
||||
self.pgp_key = config.get('sign', 'key', fallback=None)
|
||||
self.homepage = config.get(section, 'homepage', fallback=None)
|
||||
self.repository = config.get('repository', 'name')
|
||||
self.homepage = config.get(section, "homepage", fallback=None)
|
||||
self.name = config.get("repository", "name")
|
||||
|
||||
def generate(self, path: str) -> None:
|
||||
sign_section = config.get_section_name("sign", architecture)
|
||||
self.sign_targets = [SignSettings.from_option(opt) for opt in config.getlist(sign_section, "target")]
|
||||
self.pgp_key = config.get(sign_section, "key") if self.sign_targets else None
|
||||
|
||||
def generate(self, packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
generate report for the specified packages
|
||||
:param packages: list of packages to generate report
|
||||
"""
|
||||
# idea comes from https://stackoverflow.com/a/38642558
|
||||
templates_dir, template_name = os.path.split(self.template_path)
|
||||
loader = jinja2.FileSystemLoader(searchpath=templates_dir)
|
||||
loader = jinja2.FileSystemLoader(searchpath=self.template_path.parent)
|
||||
environment = jinja2.Environment(loader=loader)
|
||||
template = environment.get_template(template_name)
|
||||
template = environment.get_template(self.template_path.name)
|
||||
|
||||
packages: Dict[str, str] = {}
|
||||
for fn in sorted(os.listdir(path)):
|
||||
if not package_like(fn):
|
||||
continue
|
||||
packages[fn] = f'{self.link_path}/{fn}'
|
||||
content = [
|
||||
{
|
||||
"archive_size": pretty_size(properties.archive_size),
|
||||
"build_date": pretty_datetime(properties.build_date),
|
||||
"filename": properties.filename,
|
||||
"installed_size": pretty_size(properties.installed_size),
|
||||
"name": package,
|
||||
"version": base.version
|
||||
} for base in packages for package, properties in base.packages.items()
|
||||
]
|
||||
comparator: Callable[[Dict[str, str]], str] = lambda item: item["filename"]
|
||||
|
||||
html = template.render(
|
||||
homepage=self.homepage,
|
||||
link_path=self.link_path,
|
||||
has_package_signed=SignSettings.SignPackages in self.sign_targets,
|
||||
has_repo_signed=SignSettings.SignRepository in self.sign_targets,
|
||||
packages=packages,
|
||||
packages=sorted(content, key=comparator),
|
||||
pgp_key=self.pgp_key,
|
||||
repository=self.repository)
|
||||
repository=self.name)
|
||||
|
||||
with open(self.report_path, 'w') as out:
|
||||
out.write(html)
|
||||
self.report_path.write_text(html)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -19,20 +19,41 @@
|
||||
#
|
||||
import logging
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import ReportFailed
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.report_settings import ReportSettings
|
||||
|
||||
|
||||
class Report:
|
||||
"""
|
||||
base report generator
|
||||
:ivar architecture: repository architecture
|
||||
:ivar config: configuration instance
|
||||
:ivar logger: class logger
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
||||
self.logger = logging.getLogger('builder')
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
self.logger = logging.getLogger("builder")
|
||||
self.architecture = architecture
|
||||
self.config = config
|
||||
|
||||
@staticmethod
|
||||
def run(architecture: str, config: Configuration, target: str, path: str) -> None:
|
||||
def run(architecture: str, config: Configuration, target: str, packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
run report generation
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
:param target: target to generate report (e.g. html)
|
||||
:param packages: list of packages to generate report
|
||||
"""
|
||||
provider = ReportSettings.from_option(target)
|
||||
if provider == ReportSettings.HTML:
|
||||
from ahriman.core.report.html import HTML
|
||||
@ -41,10 +62,13 @@ class Report:
|
||||
report = Report(architecture, config)
|
||||
|
||||
try:
|
||||
report.generate(path)
|
||||
report.generate(packages)
|
||||
except Exception:
|
||||
report.logger.exception('report generation failed', exc_info=True)
|
||||
report.logger.exception(f"report generation failed for target {provider.name}")
|
||||
raise ReportFailed()
|
||||
|
||||
def generate(self, path: str) -> None:
|
||||
pass
|
||||
def generate(self, packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
generate report for the specified packages
|
||||
:param packages: list of packages to generate report
|
||||
"""
|
||||
|
@ -1,196 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
#
|
||||
# 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 logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from typing import Dict, Iterable, List, Optional
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.alpm.repo import Repo
|
||||
from ahriman.core.build_tools.task import Task
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.report.report import Report
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.upload.uploader import Uploader
|
||||
from ahriman.core.util import package_like
|
||||
from ahriman.core.watcher.client import Client
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Repository:
|
||||
|
||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
||||
self.logger = logging.getLogger('builder')
|
||||
self.architecture = architecture
|
||||
self.config = config
|
||||
|
||||
self.aur_url = config.get('alpm', 'aur_url')
|
||||
self.name = config.get('repository', 'name')
|
||||
|
||||
self.paths = RepositoryPaths(config.get('repository', 'root'), architecture)
|
||||
self.paths.create_tree()
|
||||
|
||||
self.pacman = Pacman(config)
|
||||
self.sign = GPG(architecture, config)
|
||||
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
|
||||
self.web = Client.load(architecture, config)
|
||||
|
||||
def _clear_build(self) -> None:
|
||||
for package in os.listdir(self.paths.sources):
|
||||
shutil.rmtree(os.path.join(self.paths.sources, package))
|
||||
|
||||
def _clear_manual(self) -> None:
|
||||
for package in os.listdir(self.paths.manual):
|
||||
shutil.rmtree(os.path.join(self.paths.manual, package))
|
||||
|
||||
def _clear_packages(self) -> None:
|
||||
for package in self.packages_built():
|
||||
os.remove(package)
|
||||
|
||||
def packages(self) -> List[Package]:
|
||||
result: Dict[str, Package] = {}
|
||||
for fn in os.listdir(self.paths.repository):
|
||||
if not package_like(fn):
|
||||
continue
|
||||
full_path = os.path.join(self.paths.repository, fn)
|
||||
try:
|
||||
local = Package.load(full_path, self.pacman, self.aur_url)
|
||||
result.setdefault(local.base, local).packages.update(local.packages)
|
||||
except Exception:
|
||||
self.logger.exception(f'could not load package from {fn}', exc_info=True)
|
||||
continue
|
||||
return list(result.values())
|
||||
|
||||
def packages_built(self) -> List[str]:
|
||||
return [
|
||||
os.path.join(self.paths.packages, fn)
|
||||
for fn in os.listdir(self.paths.packages)
|
||||
]
|
||||
|
||||
def process_build(self, updates: Iterable[Package]) -> List[str]:
|
||||
def build_single(package: Package) -> None:
|
||||
self.web.set_building(package.base)
|
||||
task = Task(package, self.architecture, self.config, self.paths)
|
||||
task.clone()
|
||||
built = task.build()
|
||||
for src in built:
|
||||
dst = os.path.join(self.paths.packages, os.path.basename(src))
|
||||
shutil.move(src, dst)
|
||||
|
||||
for package in updates:
|
||||
try:
|
||||
build_single(package)
|
||||
except Exception:
|
||||
self.web.set_failed(package.base)
|
||||
self.logger.exception(f'{package.base} ({self.architecture}) build exception', exc_info=True)
|
||||
continue
|
||||
self._clear_build()
|
||||
|
||||
return self.packages_built()
|
||||
|
||||
def process_remove(self, packages: Iterable[str]) -> str:
|
||||
def remove_single(package: str) -> None:
|
||||
try:
|
||||
self.repo.remove(package, package)
|
||||
except Exception:
|
||||
self.logger.exception(f'could not remove {package}', exc_info=True)
|
||||
|
||||
for local in self.packages():
|
||||
if local.base in packages:
|
||||
to_remove = local.packages
|
||||
elif local.packages.intersection(packages):
|
||||
to_remove = local.packages.intersection(packages)
|
||||
else:
|
||||
to_remove = set()
|
||||
self.web.remove(local.base, to_remove)
|
||||
for package in to_remove:
|
||||
remove_single(package)
|
||||
|
||||
return self.repo.repo_path
|
||||
|
||||
def process_report(self, targets: Optional[Iterable[str]]) -> None:
|
||||
if targets is None:
|
||||
targets = self.config.getlist('report', 'target')
|
||||
for target in targets:
|
||||
Report.run(self.architecture, self.config, target, self.paths.repository)
|
||||
|
||||
def process_sync(self, targets: Optional[Iterable[str]]) -> None:
|
||||
if targets is None:
|
||||
targets = self.config.getlist('upload', 'target')
|
||||
for target in targets:
|
||||
Uploader.run(self.architecture, self.config, target, self.paths.repository)
|
||||
|
||||
def process_update(self, packages: Iterable[str]) -> str:
|
||||
for package in packages:
|
||||
local = Package.load(package, self.pacman, self.aur_url) # we will use it for status reports
|
||||
try:
|
||||
files = self.sign.sign_package(package, local.base)
|
||||
for src in files:
|
||||
dst = os.path.join(self.paths.repository, os.path.basename(src))
|
||||
shutil.move(src, dst)
|
||||
package_fn = os.path.join(self.paths.repository, os.path.basename(package))
|
||||
self.repo.add(package_fn)
|
||||
self.web.set_success(local)
|
||||
except Exception:
|
||||
self.logger.exception(f'could not process {package}', exc_info=True)
|
||||
self.web.set_failed(local.base)
|
||||
self._clear_packages()
|
||||
|
||||
return self.repo.repo_path
|
||||
|
||||
def updates_aur(self, no_vcs: bool) -> List[Package]:
|
||||
result: List[Package] = []
|
||||
|
||||
build_section = self.config.get_section_name('build', self.architecture)
|
||||
ignore_list = self.config.getlist(build_section, 'ignore_packages')
|
||||
|
||||
for local in self.packages():
|
||||
if local.base in ignore_list:
|
||||
continue
|
||||
if local.is_vcs and no_vcs:
|
||||
continue
|
||||
|
||||
try:
|
||||
remote = Package.load(local.base, self.pacman, self.aur_url)
|
||||
if local.is_outdated(remote):
|
||||
result.append(remote)
|
||||
self.web.set_pending(local.base)
|
||||
except Exception:
|
||||
self.web.set_failed(local.base)
|
||||
self.logger.exception(f'could not load remote package {local.base}', exc_info=True)
|
||||
continue
|
||||
|
||||
return result
|
||||
|
||||
def updates_manual(self) -> List[Package]:
|
||||
result: List[Package] = []
|
||||
|
||||
for fn in os.listdir(self.paths.manual):
|
||||
try:
|
||||
local = Package.load(os.path.join(self.paths.manual, fn), self.pacman, self.aur_url)
|
||||
result.append(local)
|
||||
self.web.set_unknown(local)
|
||||
except Exception:
|
||||
self.logger.exception(f'could not add package from {fn}', exc_info=True)
|
||||
self._clear_manual()
|
||||
|
||||
return result
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
78
src/ahriman/core/repository/cleaner.py
Normal file
78
src/ahriman/core/repository/cleaner.py
Normal file
@ -0,0 +1,78 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 shutil
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from ahriman.core.repository.properties import Properties
|
||||
|
||||
|
||||
class Cleaner(Properties):
|
||||
"""
|
||||
trait to clean common repository objects
|
||||
"""
|
||||
|
||||
def packages_built(self) -> List[Path]:
|
||||
"""
|
||||
get list of files in built packages directory
|
||||
:return: list of filenames from the directory
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def clear_build(self) -> None:
|
||||
"""
|
||||
clear sources directory
|
||||
"""
|
||||
self.logger.info("clear package sources directory")
|
||||
for package in self.paths.sources.iterdir():
|
||||
shutil.rmtree(package)
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""
|
||||
clear cache directory
|
||||
"""
|
||||
self.logger.info("clear packages sources cache directory")
|
||||
for package in self.paths.cache.iterdir():
|
||||
shutil.rmtree(package)
|
||||
|
||||
def clear_chroot(self) -> None:
|
||||
"""
|
||||
clear cache directory. Warning: this method is architecture independent and will clear every chroot
|
||||
"""
|
||||
self.logger.info("clear build chroot directory")
|
||||
for chroot in self.paths.chroot.iterdir():
|
||||
shutil.rmtree(chroot)
|
||||
|
||||
def clear_manual(self) -> None:
|
||||
"""
|
||||
clear directory with manual package updates
|
||||
"""
|
||||
self.logger.info("clear manual packages")
|
||||
for package in self.paths.manual.iterdir():
|
||||
shutil.rmtree(package)
|
||||
|
||||
def clear_packages(self) -> None:
|
||||
"""
|
||||
clear directory with built packages (NOT repository itself)
|
||||
"""
|
||||
self.logger.info("clear built packages directory")
|
||||
for package in self.packages_built():
|
||||
package.unlink()
|
161
src/ahriman/core/repository/executor.py
Normal file
161
src/ahriman/core/repository/executor.py
Normal file
@ -0,0 +1,161 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 shutil
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, List, Optional
|
||||
|
||||
from ahriman.core.build_tools.task import Task
|
||||
from ahriman.core.report.report import Report
|
||||
from ahriman.core.repository.cleaner import Cleaner
|
||||
from ahriman.core.upload.uploader import Uploader
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class Executor(Cleaner):
|
||||
"""
|
||||
trait for common repository update processes
|
||||
"""
|
||||
|
||||
def packages(self) -> List[Package]:
|
||||
"""
|
||||
generate list of repository packages
|
||||
:return: list of packages properties
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def process_build(self, updates: Iterable[Package]) -> List[Path]:
|
||||
"""
|
||||
build packages
|
||||
:param updates: list of packages properties to build
|
||||
:return: `packages_built`
|
||||
"""
|
||||
def build_single(package: Package) -> None:
|
||||
self.reporter.set_building(package.base)
|
||||
task = Task(package, self.architecture, self.config, self.paths)
|
||||
task.init()
|
||||
built = task.build()
|
||||
for src in built:
|
||||
dst = self.paths.packages / src.name
|
||||
shutil.move(src, dst)
|
||||
|
||||
for single in updates:
|
||||
try:
|
||||
build_single(single)
|
||||
except Exception:
|
||||
self.reporter.set_failed(single.base)
|
||||
self.logger.exception(f"{single.base} ({self.architecture}) build exception")
|
||||
self.clear_build()
|
||||
|
||||
return self.packages_built()
|
||||
|
||||
def process_remove(self, packages: Iterable[str]) -> Path:
|
||||
"""
|
||||
remove packages from list
|
||||
:param packages: list of package names or bases to remove
|
||||
:return: path to repository database
|
||||
"""
|
||||
def remove_single(package: str, fn: Path) -> None:
|
||||
try:
|
||||
self.repo.remove(package, fn)
|
||||
except Exception:
|
||||
self.logger.exception(f"could not remove {package}")
|
||||
|
||||
requested = set(packages)
|
||||
for local in self.packages():
|
||||
if local.base in packages or all(package in requested for package in local.packages):
|
||||
to_remove = {
|
||||
package: Path(properties.filename)
|
||||
for package, properties in local.packages.items()
|
||||
if properties.filename is not None
|
||||
}
|
||||
self.reporter.remove(local.base) # we only update status page in case of base removal
|
||||
elif requested.intersection(local.packages.keys()):
|
||||
to_remove = {
|
||||
package: Path(properties.filename)
|
||||
for package, properties in local.packages.items()
|
||||
if package in requested and properties.filename is not None
|
||||
}
|
||||
else:
|
||||
to_remove = dict()
|
||||
for package, filename in to_remove.items():
|
||||
remove_single(package, filename)
|
||||
|
||||
return self.repo.repo_path
|
||||
|
||||
def process_report(self, targets: Optional[Iterable[str]]) -> None:
|
||||
"""
|
||||
generate reports
|
||||
:param targets: list of targets to generate reports. Configuration option will be used if it is not set
|
||||
"""
|
||||
if targets is None:
|
||||
targets = self.config.getlist("report", "target")
|
||||
for target in targets:
|
||||
Report.run(self.architecture, self.config, target, self.packages())
|
||||
|
||||
def process_sync(self, targets: Optional[Iterable[str]]) -> None:
|
||||
"""
|
||||
process synchronization to remote servers
|
||||
:param targets: list of targets to sync. Configuration option will be used if it is not set
|
||||
"""
|
||||
if targets is None:
|
||||
targets = self.config.getlist("upload", "target")
|
||||
for target in targets:
|
||||
Uploader.run(self.architecture, self.config, target, self.paths.repository)
|
||||
|
||||
def process_update(self, packages: Iterable[Path]) -> Path:
|
||||
"""
|
||||
sign packages, add them to repository and update repository database
|
||||
:param packages: list of filenames to run
|
||||
:return: path to repository database
|
||||
"""
|
||||
def update_single(fn: Optional[str], base: str) -> None:
|
||||
if fn is None:
|
||||
self.logger.warning(f"received empty package name for base {base}")
|
||||
return # suppress type checking, it never can be none actually
|
||||
# in theory it might be NOT packages directory, but we suppose it is
|
||||
full_path = self.paths.packages / fn
|
||||
files = self.sign.sign_package(full_path, base)
|
||||
for src in files:
|
||||
dst = self.paths.repository / src.name
|
||||
shutil.move(src, dst)
|
||||
package_path = self.paths.repository / fn
|
||||
self.repo.add(package_path)
|
||||
|
||||
# we are iterating over bases, not single packages
|
||||
updates: Dict[str, Package] = {}
|
||||
for filename in packages:
|
||||
try:
|
||||
local = Package.load(filename, self.pacman, self.aur_url)
|
||||
updates.setdefault(local.base, local).packages.update(local.packages)
|
||||
except Exception:
|
||||
self.logger.exception(f"could not load package from {filename}")
|
||||
|
||||
for local in updates.values():
|
||||
try:
|
||||
for description in local.packages.values():
|
||||
update_single(description.filename, local.base)
|
||||
self.reporter.set_success(local)
|
||||
except Exception:
|
||||
self.reporter.set_failed(local.base)
|
||||
self.logger.exception(f"could not process {local.base}")
|
||||
self.clear_packages()
|
||||
|
||||
return self.repo.repo_path
|
59
src/ahriman/core/repository/properties.py
Normal file
59
src/ahriman/core/repository/properties.py
Normal file
@ -0,0 +1,59 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 logging
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.alpm.repo import Repo
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Properties:
|
||||
"""
|
||||
repository internal objects holder
|
||||
:ivar architecture: repository architecture
|
||||
:ivar aur_url: base AUR url
|
||||
:ivar config: configuration instance
|
||||
:ivar logger: class logger
|
||||
:ivar name: repository name
|
||||
:ivar pacman: alpm wrapper instance
|
||||
:ivar paths: repository paths instance
|
||||
:ivar repo: repo commands wrapper instance
|
||||
:ivar reporter: build status reporter instance
|
||||
:ivar sign: GPG wrapper instance
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
||||
self.logger = logging.getLogger("builder")
|
||||
self.architecture = architecture
|
||||
self.config = config
|
||||
|
||||
self.aur_url = config.get("alpm", "aur_url")
|
||||
self.name = config.get("repository", "name")
|
||||
|
||||
self.paths = RepositoryPaths(config.getpath("repository", "root"), architecture)
|
||||
self.paths.create_tree()
|
||||
|
||||
self.pacman = Pacman(config)
|
||||
self.sign = GPG(architecture, config)
|
||||
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
|
||||
self.reporter = Client.load(architecture, config)
|
56
src/ahriman/core/repository/repository.py
Normal file
56
src/ahriman/core/repository/repository.py
Normal file
@ -0,0 +1,56 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
from ahriman.core.repository.executor import Executor
|
||||
from ahriman.core.repository.update_handler import UpdateHandler
|
||||
from ahriman.core.util import package_like
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class Repository(Executor, UpdateHandler):
|
||||
"""
|
||||
base repository control class
|
||||
"""
|
||||
|
||||
def packages(self) -> List[Package]:
|
||||
"""
|
||||
generate list of repository packages
|
||||
:return: list of packages properties
|
||||
"""
|
||||
result: Dict[str, Package] = {}
|
||||
for full_path in self.paths.repository.iterdir():
|
||||
if not package_like(full_path):
|
||||
continue
|
||||
try:
|
||||
local = Package.load(full_path, self.pacman, self.aur_url)
|
||||
result.setdefault(local.base, local).packages.update(local.packages)
|
||||
except Exception:
|
||||
self.logger.exception(f"could not load package from {full_path}")
|
||||
continue
|
||||
return list(result.values())
|
||||
|
||||
def packages_built(self) -> List[Path]:
|
||||
"""
|
||||
get list of files in built packages directory
|
||||
:return: list of filenames from the directory
|
||||
"""
|
||||
return list(self.paths.packages.iterdir())
|
90
src/ahriman/core/repository/update_handler.py
Normal file
90
src/ahriman/core/repository/update_handler.py
Normal file
@ -0,0 +1,90 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 typing import Iterable, List
|
||||
|
||||
from ahriman.core.repository.cleaner import Cleaner
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class UpdateHandler(Cleaner):
|
||||
"""
|
||||
trait to get package update list
|
||||
"""
|
||||
|
||||
def packages(self) -> List[Package]:
|
||||
"""
|
||||
generate list of repository packages
|
||||
:return: list of packages properties
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def updates_aur(self, filter_packages: Iterable[str], no_vcs: bool) -> List[Package]:
|
||||
"""
|
||||
check AUR for updates
|
||||
:param filter_packages: do not check every package just specified in the list
|
||||
:param no_vcs: do not check VCS packages
|
||||
:return: list of packages which are out-of-dated
|
||||
"""
|
||||
result: List[Package] = []
|
||||
|
||||
build_section = self.config.get_section_name("build", self.architecture)
|
||||
ignore_list = self.config.getlist(build_section, "ignore_packages")
|
||||
|
||||
for local in self.packages():
|
||||
if local.base in ignore_list:
|
||||
continue
|
||||
if local.is_vcs and no_vcs:
|
||||
continue
|
||||
if filter_packages and local.base not in filter_packages:
|
||||
continue
|
||||
|
||||
try:
|
||||
remote = Package.load(local.base, self.pacman, self.aur_url)
|
||||
if local.is_outdated(remote, self.paths):
|
||||
self.reporter.set_pending(local.base)
|
||||
result.append(remote)
|
||||
except Exception:
|
||||
self.reporter.set_failed(local.base)
|
||||
self.logger.exception(f"could not load remote package {local.base}")
|
||||
continue
|
||||
|
||||
return result
|
||||
|
||||
def updates_manual(self) -> List[Package]:
|
||||
"""
|
||||
check for packages for which manual update has been requested
|
||||
:return: list of packages which are out-of-dated
|
||||
"""
|
||||
result: List[Package] = []
|
||||
known_bases = {package.base for package in self.packages()}
|
||||
|
||||
for fn in self.paths.manual.iterdir():
|
||||
try:
|
||||
local = Package.load(fn, self.pacman, self.aur_url)
|
||||
result.append(local)
|
||||
if local.base not in known_bases:
|
||||
self.reporter.set_unknown(local)
|
||||
else:
|
||||
self.reporter.set_pending(local.base)
|
||||
except Exception:
|
||||
self.logger.exception(f"could not add package from {fn}")
|
||||
self.clear_manual()
|
||||
|
||||
return result
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -18,8 +18,8 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
import os
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
@ -29,41 +29,80 @@ from ahriman.models.sign_settings import SignSettings
|
||||
|
||||
|
||||
class GPG:
|
||||
"""
|
||||
gnupg wrapper
|
||||
:ivar architecture: repository architecture
|
||||
:ivar config: configuration instance
|
||||
:ivar default_key: default PGP key ID to use
|
||||
:ivar logger: class logger
|
||||
:ivar target: list of targets to sign (repository, package etc)
|
||||
"""
|
||||
|
||||
_check_output = check_output
|
||||
|
||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
||||
self.logger = logging.getLogger('build_details')
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
self.logger = logging.getLogger("build_details")
|
||||
self.config = config
|
||||
self.section = config.get_section_name('sign', architecture)
|
||||
self.target = [SignSettings.from_option(opt) for opt in config.getlist(self.section, 'target')]
|
||||
self.default_key = config.get(self.section, 'key') if self.target else ''
|
||||
self.section = config.get_section_name("sign", architecture)
|
||||
self.target = {SignSettings.from_option(opt) for opt in config.getlist(self.section, "target")}
|
||||
self.default_key = config.get(self.section, "key") if self.target else ""
|
||||
|
||||
@property
|
||||
def repository_sign_args(self) -> List[str]:
|
||||
"""
|
||||
:return: command line arguments for repo-add command to sign database
|
||||
"""
|
||||
if SignSettings.SignRepository not in self.target:
|
||||
return []
|
||||
return ['--sign', '--key', self.default_key]
|
||||
return ["--sign", "--key", self.default_key]
|
||||
|
||||
def process(self, path: str, key: str) -> List[str]:
|
||||
check_output(
|
||||
*self.sign_cmd(path, key),
|
||||
exception=BuildFailed(path),
|
||||
cwd=os.path.dirname(path),
|
||||
@staticmethod
|
||||
def sign_cmd(path: Path, key: str) -> List[str]:
|
||||
"""
|
||||
gpg command to run
|
||||
:param path: path to file to sign
|
||||
:param key: PGP key ID
|
||||
:return: gpg command with all required arguments
|
||||
"""
|
||||
return ["gpg", "-u", key, "-b", str(path)]
|
||||
|
||||
def process(self, path: Path, key: str) -> List[Path]:
|
||||
"""
|
||||
gpg command wrapper
|
||||
:param path: path to file to sign
|
||||
:param key: PGP key ID
|
||||
:return: list of generated files including original file
|
||||
"""
|
||||
GPG._check_output(
|
||||
*GPG.sign_cmd(path, key),
|
||||
exception=BuildFailed(path.name),
|
||||
logger=self.logger)
|
||||
return [path, f'{path}.sig']
|
||||
return [path, path.parent / f"{path.name}.sig"]
|
||||
|
||||
def sign_cmd(self, path: str, key: str) -> List[str]:
|
||||
cmd = ['gpg']
|
||||
cmd.extend(['-u', key])
|
||||
cmd.extend(['-b', path])
|
||||
return cmd
|
||||
|
||||
def sign_package(self, path: str, base: str) -> List[str]:
|
||||
def sign_package(self, path: Path, base: str) -> List[Path]:
|
||||
"""
|
||||
sign package if required by configuration
|
||||
:param path: path to file to sign
|
||||
:param base: package base required to check for key overrides
|
||||
:return: list of generated files including original file
|
||||
"""
|
||||
if SignSettings.SignPackages not in self.target:
|
||||
return [path]
|
||||
key = self.config.get(self.section, f'key_{base}', fallback=self.default_key)
|
||||
key = self.config.get(self.section, f"key_{base}", fallback=self.default_key)
|
||||
return self.process(path, key)
|
||||
|
||||
def sign_repository(self, path: str) -> List[str]:
|
||||
def sign_repository(self, path: Path) -> List[Path]:
|
||||
"""
|
||||
sign repository if required by configuration
|
||||
:note: more likely you just want to pass `repository_sign_args` to repo wrapper
|
||||
:param path: path to repository database
|
||||
:return: list of generated files including original file
|
||||
"""
|
||||
if SignSettings.SignRepository not in self.target:
|
||||
return [path]
|
||||
return self.process(path, self.default_key)
|
||||
return self.process(path, self.default_key)
|
||||
|
19
src/ahriman/core/status/__init__.py
Normal file
19
src/ahriman/core/status/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
#
|
||||
# Copyright (c) 2021 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/>.
|
||||
#
|
128
src/ahriman/core/status/client.py
Normal file
128
src/ahriman/core/status/client.py
Normal file
@ -0,0 +1,128 @@
|
||||
#
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class Client:
|
||||
"""
|
||||
base build status reporter client
|
||||
"""
|
||||
|
||||
def add(self, package: Package, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
add new package with status
|
||||
:param package: package properties
|
||||
:param status: current package build status
|
||||
"""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
|
||||
"""
|
||||
get package status
|
||||
:param base: package base to get
|
||||
:return: list of current package description and status if it has been found
|
||||
"""
|
||||
del base
|
||||
return []
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_self(self) -> BuildStatus:
|
||||
"""
|
||||
get ahriman status itself
|
||||
:return: current ahriman status
|
||||
"""
|
||||
return BuildStatus()
|
||||
|
||||
def remove(self, base: str) -> None:
|
||||
"""
|
||||
remove packages from watcher
|
||||
:param base: package base to remove
|
||||
"""
|
||||
|
||||
def update(self, base: str, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
update package build status. Unlike `add` it does not update package properties
|
||||
:param base: package base to update
|
||||
:param status: current package build status
|
||||
"""
|
||||
|
||||
def update_self(self, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
update ahriman status itself
|
||||
:param status: current ahriman status
|
||||
"""
|
||||
|
||||
def set_building(self, base: str) -> None:
|
||||
"""
|
||||
set package status to building
|
||||
:param base: package base to update
|
||||
"""
|
||||
return self.update(base, BuildStatusEnum.Building)
|
||||
|
||||
def set_failed(self, base: str) -> None:
|
||||
"""
|
||||
set package status to failed
|
||||
:param base: package base to update
|
||||
"""
|
||||
return self.update(base, BuildStatusEnum.Failed)
|
||||
|
||||
def set_pending(self, base: str) -> None:
|
||||
"""
|
||||
set package status to pending
|
||||
:param base: package base to update
|
||||
"""
|
||||
return self.update(base, BuildStatusEnum.Pending)
|
||||
|
||||
def set_success(self, package: Package) -> None:
|
||||
"""
|
||||
set package status to success
|
||||
:param package: current package properties
|
||||
"""
|
||||
return self.add(package, BuildStatusEnum.Success)
|
||||
|
||||
def set_unknown(self, package: Package) -> None:
|
||||
"""
|
||||
set package status to unknown
|
||||
:param package: current package properties
|
||||
"""
|
||||
return self.add(package, BuildStatusEnum.Unknown)
|
||||
|
||||
@staticmethod
|
||||
def load(architecture: str, config: Configuration) -> Client:
|
||||
"""
|
||||
load client from settings
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
:return: client according to current settings
|
||||
"""
|
||||
section = config.get_section_name("web", architecture)
|
||||
host = config.get(section, "host", fallback=None)
|
||||
port = config.getint(section, "port", fallback=None)
|
||||
if host is None or port is None:
|
||||
return Client()
|
||||
|
||||
from ahriman.core.status.web_client import WebClient
|
||||
return WebClient(host, port)
|
166
src/ahriman/core/status/watcher.py
Normal file
166
src/ahriman/core/status/watcher.py
Normal file
@ -0,0 +1,166 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 json
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import UnknownPackage
|
||||
from ahriman.core.repository.repository import Repository
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class Watcher:
|
||||
"""
|
||||
package status watcher
|
||||
:ivar architecture: repository architecture
|
||||
:ivar known: list of known packages. For the most cases `packages` should be used instead
|
||||
:ivar logger: class logger
|
||||
:ivar repository: repository object
|
||||
:ivar status: daemon status
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
self.logger = logging.getLogger("http")
|
||||
|
||||
self.architecture = architecture
|
||||
self.repository = Repository(architecture, config)
|
||||
|
||||
self.known: Dict[str, Tuple[Package, BuildStatus]] = {}
|
||||
self.status = BuildStatus()
|
||||
|
||||
@property
|
||||
def cache_path(self) -> Path:
|
||||
"""
|
||||
:return: path to dump with json cache
|
||||
"""
|
||||
return self.repository.paths.root / "status_cache.json"
|
||||
|
||||
@property
|
||||
def packages(self) -> List[Tuple[Package, BuildStatus]]:
|
||||
"""
|
||||
:return: list of packages together with their statuses
|
||||
"""
|
||||
return list(self.known.values())
|
||||
|
||||
def _cache_load(self) -> None:
|
||||
"""
|
||||
update current state from cache
|
||||
"""
|
||||
def parse_single(properties: Dict[str, Any]) -> None:
|
||||
package = Package.from_json(properties["package"])
|
||||
status = BuildStatus.from_json(properties["status"])
|
||||
if package.base in self.known:
|
||||
self.known[package.base] = (package, status)
|
||||
|
||||
if not self.cache_path.is_file():
|
||||
return
|
||||
with self.cache_path.open() as cache:
|
||||
try:
|
||||
dump = json.load(cache)
|
||||
except Exception:
|
||||
self.logger.exception("cannot parse json from file")
|
||||
dump = {}
|
||||
for item in dump.get("packages", []):
|
||||
try:
|
||||
parse_single(item)
|
||||
except Exception:
|
||||
self.logger.exception(f"cannot parse item f{item} to package")
|
||||
|
||||
def _cache_save(self) -> None:
|
||||
"""
|
||||
dump current cache to filesystem
|
||||
"""
|
||||
dump = {
|
||||
"packages": [
|
||||
{
|
||||
"package": package.view(),
|
||||
"status": status.view()
|
||||
} for package, status in self.packages
|
||||
]
|
||||
}
|
||||
try:
|
||||
with self.cache_path.open("w") as cache:
|
||||
json.dump(dump, cache)
|
||||
except Exception:
|
||||
self.logger.exception("cannot dump cache")
|
||||
|
||||
def get(self, base: str) -> Tuple[Package, BuildStatus]:
|
||||
"""
|
||||
get current package base build status
|
||||
:return: package and its status
|
||||
"""
|
||||
try:
|
||||
return self.known[base]
|
||||
except KeyError:
|
||||
raise UnknownPackage(base)
|
||||
|
||||
def load(self) -> None:
|
||||
"""
|
||||
load packages from local repository. In case if last status is known, it will use it
|
||||
"""
|
||||
for package in self.repository.packages():
|
||||
# get status of build or assign unknown
|
||||
current = self.known.get(package.base)
|
||||
if current is None:
|
||||
status = BuildStatus()
|
||||
else:
|
||||
_, status = current
|
||||
self.known[package.base] = (package, status)
|
||||
self._cache_load()
|
||||
|
||||
def remove(self, base: str) -> None:
|
||||
"""
|
||||
remove package base from known list if any
|
||||
:param base: package base
|
||||
"""
|
||||
self.known.pop(base, None)
|
||||
self._cache_save()
|
||||
|
||||
def update(self, base: str, status: BuildStatusEnum, package: Optional[Package]) -> None:
|
||||
"""
|
||||
update package status and description
|
||||
:param base: package base to update
|
||||
:param status: new build status
|
||||
:param package: optional new package description. In case if not set current properties will be used
|
||||
"""
|
||||
if package is None:
|
||||
try:
|
||||
package, _ = self.known[base]
|
||||
except KeyError:
|
||||
raise UnknownPackage(base)
|
||||
full_status = BuildStatus(status)
|
||||
self.known[base] = (package, full_status)
|
||||
self._cache_save()
|
||||
|
||||
def update_self(self, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
update service status
|
||||
:param status: new service status
|
||||
"""
|
||||
self.status = BuildStatus(status)
|
162
src/ahriman/core/status/web_client.py
Normal file
162
src/ahriman/core/status/web_client.py
Normal file
@ -0,0 +1,162 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 logging
|
||||
import requests
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.models.build_status import BuildStatusEnum, BuildStatus
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class WebClient(Client):
|
||||
"""
|
||||
build status reporter web client
|
||||
:ivar host: host of web service
|
||||
:ivar logger: class logger
|
||||
:ivar port: port of web service
|
||||
"""
|
||||
|
||||
def __init__(self, host: str, port: int) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param host: host of web service
|
||||
:param port: port of web service
|
||||
"""
|
||||
self.logger = logging.getLogger("http")
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
def _ahriman_url(self) -> str:
|
||||
"""
|
||||
url generator
|
||||
:return: full url for web service for ahriman service itself
|
||||
"""
|
||||
return f"http://{self.host}:{self.port}/api/v1/ahriman"
|
||||
|
||||
def _package_url(self, base: str = "") -> str:
|
||||
"""
|
||||
url generator
|
||||
:param base: package base to generate url
|
||||
:return: full url of web service for specific package base
|
||||
"""
|
||||
return f"http://{self.host}:{self.port}/api/v1/packages/{base}"
|
||||
|
||||
def add(self, package: Package, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
add new package with status
|
||||
:param package: package properties
|
||||
:param status: current package build status
|
||||
"""
|
||||
payload = {
|
||||
"status": status.value,
|
||||
"package": package.view()
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(self._package_url(package.base), json=payload)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not add {package.base}: {e.response.text}")
|
||||
except Exception:
|
||||
self.logger.exception(f"could not add {package.base}")
|
||||
|
||||
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
|
||||
"""
|
||||
get package status
|
||||
:param base: package base to get
|
||||
:return: list of current package description and status if it has been found
|
||||
"""
|
||||
try:
|
||||
response = requests.get(self._package_url(base or ""))
|
||||
response.raise_for_status()
|
||||
|
||||
status_json = response.json()
|
||||
return [
|
||||
(Package.from_json(package["package"]), BuildStatus.from_json(package["status"]))
|
||||
for package in status_json
|
||||
]
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not get {base}: {e.response.text}")
|
||||
except Exception:
|
||||
self.logger.exception(f"could not get {base}")
|
||||
return []
|
||||
|
||||
def get_self(self) -> BuildStatus:
|
||||
"""
|
||||
get ahriman status itself
|
||||
:return: current ahriman status
|
||||
"""
|
||||
try:
|
||||
response = requests.get(self._ahriman_url())
|
||||
response.raise_for_status()
|
||||
|
||||
status_json = response.json()
|
||||
return BuildStatus.from_json(status_json)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not get service status: {e.response.text}")
|
||||
except Exception:
|
||||
self.logger.exception("could not get service status")
|
||||
return BuildStatus()
|
||||
|
||||
def remove(self, base: str) -> None:
|
||||
"""
|
||||
remove packages from watcher
|
||||
:param base: basename to remove
|
||||
"""
|
||||
try:
|
||||
response = requests.delete(self._package_url(base))
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not delete {base}: {e.response.text}")
|
||||
except Exception:
|
||||
self.logger.exception(f"could not delete {base}")
|
||||
|
||||
def update(self, base: str, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
update package build status. Unlike `add` it does not update package properties
|
||||
:param base: package base to update
|
||||
:param status: current package build status
|
||||
"""
|
||||
payload = {"status": status.value}
|
||||
|
||||
try:
|
||||
response = requests.post(self._package_url(base), json=payload)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not update {base}: {e.response.text}")
|
||||
except Exception:
|
||||
self.logger.exception(f"could not update {base}")
|
||||
|
||||
def update_self(self, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
update ahriman status itself
|
||||
:param status: current ahriman status
|
||||
"""
|
||||
payload = {"status": status.value}
|
||||
|
||||
try:
|
||||
response = requests.post(self._ahriman_url(), json=payload)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception(f"could not update service status: {e.response.text}")
|
||||
except Exception:
|
||||
self.logger.exception("could not update service status")
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -22,54 +22,95 @@ from __future__ import annotations
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from typing import Iterable, List, Set
|
||||
from pathlib import Path
|
||||
from typing import Iterable, List, Set, Type
|
||||
|
||||
from ahriman.core.build_tools.task import Task
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class Tree:
|
||||
class Leaf:
|
||||
"""
|
||||
tree leaf implementation
|
||||
:ivar dependencies: list of package dependencies
|
||||
:ivar package: leaf package properties
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.packages: List[Leaf] = []
|
||||
def __init__(self, package: Package, dependencies: Set[str]) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param package: package properties
|
||||
:param dependencies: package dependencies
|
||||
"""
|
||||
self.package = package
|
||||
self.dependencies = dependencies
|
||||
|
||||
@property
|
||||
def items(self) -> Iterable[str]:
|
||||
"""
|
||||
:return: packages containing in this leaf
|
||||
"""
|
||||
return self.package.packages.keys()
|
||||
|
||||
@classmethod
|
||||
def load(cls: Type[Leaf], package: Package) -> Leaf:
|
||||
"""
|
||||
load leaf from package with dependencies
|
||||
:param package: package properties
|
||||
:return: loaded class
|
||||
"""
|
||||
clone_dir = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
Task.fetch(clone_dir, package.git_url)
|
||||
dependencies = Package.dependencies(clone_dir)
|
||||
finally:
|
||||
shutil.rmtree(clone_dir, ignore_errors=True)
|
||||
return cls(package, dependencies)
|
||||
|
||||
def is_root(self, packages: Iterable[Leaf]) -> bool:
|
||||
"""
|
||||
check if package depends on any other package from list of not
|
||||
:param packages: list of known leaves
|
||||
:return: True if any of packages is dependency of the leaf, False otherwise
|
||||
"""
|
||||
for leaf in packages:
|
||||
if self.dependencies.intersection(leaf.items):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class Tree:
|
||||
"""
|
||||
dependency tree implementation
|
||||
:ivar leaves: list of tree leaves
|
||||
"""
|
||||
|
||||
def __init__(self, leaves: List[Leaf]) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param leaves: leaves to build the tree
|
||||
"""
|
||||
self.leaves = leaves
|
||||
|
||||
@classmethod
|
||||
def load(cls: Type[Tree], packages: Iterable[Package]) -> Tree:
|
||||
"""
|
||||
load tree from packages
|
||||
:param packages: packages list
|
||||
:return: loaded class
|
||||
"""
|
||||
return cls([Leaf.load(package) for package in packages])
|
||||
|
||||
def levels(self) -> List[List[Package]]:
|
||||
"""
|
||||
get build levels starting from the packages which do not require any other package to build
|
||||
:return: list of packages lists
|
||||
"""
|
||||
result: List[List[Package]] = []
|
||||
|
||||
unprocessed = [leaf for leaf in self.packages]
|
||||
unprocessed = self.leaves[:]
|
||||
while unprocessed:
|
||||
result.append([leaf.package for leaf in unprocessed if leaf.is_root(unprocessed)])
|
||||
unprocessed = [leaf for leaf in unprocessed if not leaf.is_root(unprocessed)]
|
||||
|
||||
return result
|
||||
|
||||
def load(self, packages: Iterable[Package]) -> None:
|
||||
for package in packages:
|
||||
leaf = Leaf(package)
|
||||
leaf.load_dependencies()
|
||||
self.packages.append(leaf)
|
||||
|
||||
|
||||
class Leaf:
|
||||
|
||||
def __init__(self, package: Package) -> None:
|
||||
self.package = package
|
||||
self.dependencies: Set[str] = set()
|
||||
|
||||
def is_root(self, packages: Iterable[Leaf]) -> bool:
|
||||
'''
|
||||
:param packages:
|
||||
:return: true if any of packages is dependency of the leaf, false otherwise
|
||||
'''
|
||||
for package in packages:
|
||||
if package.package.packages.intersection(self.dependencies):
|
||||
return False
|
||||
return True
|
||||
|
||||
def load_dependencies(self) -> None:
|
||||
clone_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
Task.fetch(clone_dir, self.package.git_url)
|
||||
self.dependencies = Package.dependencies(clone_dir)
|
||||
finally:
|
||||
shutil.rmtree(clone_dir, ignore_errors=True)
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -17,19 +17,44 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.upload.uploader import Uploader
|
||||
from ahriman.core.util import check_output
|
||||
|
||||
|
||||
class Rsync(Uploader):
|
||||
"""
|
||||
rsync wrapper
|
||||
:ivar remote: remote address to sync
|
||||
"""
|
||||
|
||||
_check_output = check_output
|
||||
|
||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
Uploader.__init__(self, architecture, config)
|
||||
section = config.get_section_name('rsync', architecture)
|
||||
self.remote = config.get(section, 'remote')
|
||||
section = config.get_section_name("rsync", architecture)
|
||||
self.remote = config.get(section, "remote")
|
||||
|
||||
def sync(self, path: str) -> None:
|
||||
check_output('rsync', '--archive', '--verbose', '--compress', '--partial', '--progress', '--delete', path, self.remote,
|
||||
exception=None,
|
||||
logger=self.logger)
|
||||
def sync(self, path: Path) -> None:
|
||||
"""
|
||||
sync data to remote server
|
||||
:param path: local path to sync
|
||||
"""
|
||||
Rsync._check_output(
|
||||
"rsync",
|
||||
"--archive",
|
||||
"--verbose",
|
||||
"--compress",
|
||||
"--partial",
|
||||
"--delete",
|
||||
str(path),
|
||||
self.remote,
|
||||
exception=None,
|
||||
logger=self.logger)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -17,20 +17,37 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.upload.uploader import Uploader
|
||||
from ahriman.core.util import check_output
|
||||
|
||||
|
||||
class S3(Uploader):
|
||||
"""
|
||||
aws-cli wrapper
|
||||
:ivar bucket: full bucket name
|
||||
"""
|
||||
|
||||
_check_output = check_output
|
||||
|
||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
Uploader.__init__(self, architecture, config)
|
||||
section = config.get_section_name('s3', architecture)
|
||||
self.bucket = config.get(section, 'bucket')
|
||||
section = config.get_section_name("s3", architecture)
|
||||
self.bucket = config.get(section, "bucket")
|
||||
|
||||
def sync(self, path: str) -> None:
|
||||
def sync(self, path: Path) -> None:
|
||||
"""
|
||||
sync data to remote server
|
||||
:param path: local path to sync
|
||||
"""
|
||||
# TODO rewrite to boto, but it is bullshit
|
||||
check_output('aws', 's3', 'sync', '--delete', path, self.bucket,
|
||||
exception=None,
|
||||
logger=self.logger)
|
||||
S3._check_output("aws", "s3", "sync", "--quiet", "--delete", str(path), self.bucket,
|
||||
exception=None,
|
||||
logger=self.logger)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -19,20 +19,40 @@
|
||||
#
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import SyncFailed
|
||||
from ahriman.models.upload_settings import UploadSettings
|
||||
|
||||
|
||||
class Uploader:
|
||||
"""
|
||||
base remote sync class
|
||||
:ivar architecture: repository architecture
|
||||
:ivar config: configuration instance
|
||||
:ivar logger: application logger
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
||||
self.logger = logging.getLogger('builder')
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
"""
|
||||
self.logger = logging.getLogger("builder")
|
||||
self.architecture = architecture
|
||||
self.config = config
|
||||
|
||||
@staticmethod
|
||||
def run(architecture: str, config: Configuration, target: str, path: str) -> None:
|
||||
def run(architecture: str, config: Configuration, target: str, path: Path) -> None:
|
||||
"""
|
||||
run remote sync
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
:param target: target to run sync (e.g. s3)
|
||||
:param path: local path to sync
|
||||
"""
|
||||
provider = UploadSettings.from_option(target)
|
||||
if provider == UploadSettings.Rsync:
|
||||
from ahriman.core.upload.rsync import Rsync
|
||||
@ -46,8 +66,11 @@ class Uploader:
|
||||
try:
|
||||
uploader.sync(path)
|
||||
except Exception:
|
||||
uploader.logger.exception('remote sync failed', exc_info=True)
|
||||
uploader.logger.exception(f"remote sync failed for {provider.name}")
|
||||
raise SyncFailed()
|
||||
|
||||
def sync(self, path: str) -> None:
|
||||
pass
|
||||
def sync(self, path: Path) -> None:
|
||||
"""
|
||||
sync data to remote server
|
||||
:param path: local path to sync
|
||||
"""
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -17,27 +17,78 @@
|
||||
# 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 datetime
|
||||
import subprocess
|
||||
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from ahriman.core.exceptions import InvalidOption
|
||||
|
||||
|
||||
def check_output(*args: str, exception: Optional[Exception],
|
||||
cwd = None, stderr: int = subprocess.STDOUT,
|
||||
logger: Optional[Logger] = None) -> str:
|
||||
cwd: Optional[Path] = None, logger: Optional[Logger] = None) -> str:
|
||||
"""
|
||||
subprocess wrapper
|
||||
:param args: command line arguments
|
||||
:param exception: exception which has to be reraised instead of default subprocess exception
|
||||
:param cwd: current working directory
|
||||
:param logger: logger to log command result if required
|
||||
:return: command output
|
||||
"""
|
||||
try:
|
||||
result = subprocess.check_output(args, cwd=cwd, stderr=stderr).decode('utf8').strip()
|
||||
result = subprocess.check_output(args, cwd=cwd, stderr=subprocess.STDOUT).decode("utf8").strip()
|
||||
if logger is not None:
|
||||
for line in result.splitlines():
|
||||
logger.debug(line)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.output is not None and logger is not None:
|
||||
for line in e.output.decode('utf8').splitlines():
|
||||
for line in e.output.decode("utf8").splitlines():
|
||||
logger.debug(line)
|
||||
raise exception or e
|
||||
return result
|
||||
|
||||
|
||||
def package_like(filename: str) -> bool:
|
||||
return '.pkg.' in filename and not filename.endswith('.sig')
|
||||
def package_like(filename: Path) -> bool:
|
||||
"""
|
||||
check if file looks like package
|
||||
:param filename: name of file to check
|
||||
:return: True in case if name contains `.pkg.` and not signature, False otherwise
|
||||
"""
|
||||
name = filename.name
|
||||
return ".pkg." in name and not name.endswith(".sig")
|
||||
|
||||
|
||||
def pretty_datetime(timestamp: Optional[int]) -> str:
|
||||
"""
|
||||
convert datetime object to string
|
||||
:param timestamp: datetime to convert
|
||||
:return: pretty printable datetime as string
|
||||
"""
|
||||
return "" if timestamp is None else datetime.datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def pretty_size(size: Optional[float], level: int = 0) -> str:
|
||||
"""
|
||||
convert size to string
|
||||
:param size: size to convert
|
||||
:param level: represents current units, 0 is B, 1 is KiB etc
|
||||
:return: pretty printable size as string
|
||||
"""
|
||||
def str_level() -> str:
|
||||
if level == 0:
|
||||
return "B"
|
||||
if level == 1:
|
||||
return "KiB"
|
||||
if level == 2:
|
||||
return "MiB"
|
||||
if level == 3:
|
||||
return "GiB"
|
||||
raise InvalidOption(level) # must never happen actually
|
||||
|
||||
if size is None:
|
||||
return ""
|
||||
if size < 1024 or level == 3:
|
||||
return f"{size:.1f} {str_level()}"
|
||||
return pretty_size(size / 1024, level + 1)
|
||||
|
@ -1,116 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from typing import Any, Dict, Set
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class Client:
|
||||
|
||||
def add(self, package: Package, status: BuildStatusEnum) -> None:
|
||||
pass
|
||||
|
||||
def remove(self, base: str, packages: Set[str]) -> None:
|
||||
pass
|
||||
|
||||
def update(self, base: str, status: BuildStatusEnum) -> None:
|
||||
pass
|
||||
|
||||
def set_building(self, base: str) -> None:
|
||||
return self.update(base, BuildStatusEnum.Building)
|
||||
|
||||
def set_failed(self, base: str) -> None:
|
||||
return self.update(base, BuildStatusEnum.Failed)
|
||||
|
||||
def set_pending(self, base: str) -> None:
|
||||
return self.update(base, BuildStatusEnum.Pending)
|
||||
|
||||
def set_success(self, package: Package) -> None:
|
||||
return self.add(package, BuildStatusEnum.Success)
|
||||
|
||||
def set_unknown(self, package: Package) -> None:
|
||||
return self.add(package, BuildStatusEnum.Unknown)
|
||||
|
||||
@staticmethod
|
||||
def load(architecture: str, config: Configuration) -> Client:
|
||||
section = config.get_section_name('web', architecture)
|
||||
host = config.get(section, 'host', fallback=None)
|
||||
port = config.getint(section, 'port', fallback=None)
|
||||
if host is None or port is None:
|
||||
return Client()
|
||||
return WebClient(host, port)
|
||||
|
||||
|
||||
class WebClient(Client):
|
||||
|
||||
def __init__(self, host: str, port: int) -> None:
|
||||
self.logger = logging.getLogger('http')
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
def _url(self, base: str) -> str:
|
||||
return f'http://{self.host}:{self.port}/api/v1/packages/{base}'
|
||||
|
||||
def add(self, package: Package, status: BuildStatusEnum) -> None:
|
||||
import requests
|
||||
|
||||
payload: Dict[str, Any] = {
|
||||
'status': status.value,
|
||||
'package': {
|
||||
'base': package.base,
|
||||
'packages': [p for p in package.packages],
|
||||
'version': package.version,
|
||||
'aur_url': package.aur_url
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(self._url(package.base), json=payload)
|
||||
response.raise_for_status()
|
||||
except:
|
||||
self.logger.exception(f'could not add {package.base}', exc_info=True)
|
||||
|
||||
def remove(self, base: str, packages: Set[str]) -> None:
|
||||
if not packages:
|
||||
return
|
||||
import requests
|
||||
|
||||
try:
|
||||
response = requests.delete(self._url(base))
|
||||
response.raise_for_status()
|
||||
except:
|
||||
self.logger.exception(f'could not delete {base}', exc_info=True)
|
||||
|
||||
def update(self, base: str, status: BuildStatusEnum) -> None:
|
||||
import requests
|
||||
|
||||
payload: Dict[str, Any] = {'status': status.value}
|
||||
|
||||
try:
|
||||
response = requests.post(self._url(base), json=payload)
|
||||
response.raise_for_status()
|
||||
except:
|
||||
self.logger.exception(f'could not update {base}', exc_info=True)
|
@ -1,57 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
#
|
||||
# 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 typing import Dict, List, Optional, Tuple
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class Watcher:
|
||||
|
||||
def __init__(self, architecture: str, config: Configuration) -> None:
|
||||
self.architecture = architecture
|
||||
self.repository = Repository(architecture, config)
|
||||
|
||||
self.known: Dict[str, Tuple[Package, BuildStatus]] = {}
|
||||
|
||||
@property
|
||||
def packages(self) -> List[Tuple[Package, BuildStatus]]:
|
||||
return [pair for pair in self.known.values()]
|
||||
|
||||
def load(self) -> None:
|
||||
for package in self.repository.packages():
|
||||
# get status of build or assign unknown
|
||||
current = self.known.get(package.base)
|
||||
if current is None:
|
||||
status = BuildStatus()
|
||||
else:
|
||||
_, status = current
|
||||
self.known[package.base] = (package, status)
|
||||
|
||||
def remove(self, base: str) -> None:
|
||||
self.known.pop(base, None)
|
||||
|
||||
def update(self, base: str, status: BuildStatusEnum, package: Optional[Package]) -> None:
|
||||
if package is None:
|
||||
package, _ = self.known[base]
|
||||
full_status = BuildStatus(status)
|
||||
self.known[base] = (package, full_status)
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -17,27 +17,104 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
from enum import Enum
|
||||
from typing import Optional, Union
|
||||
from typing import Any, Dict, Optional, Type, Union
|
||||
|
||||
from ahriman.core.util import pretty_datetime
|
||||
|
||||
|
||||
class BuildStatusEnum(Enum):
|
||||
Unknown = 'unknown'
|
||||
Pending = 'pending'
|
||||
Building = 'building'
|
||||
Failed = 'failed'
|
||||
Success = 'success'
|
||||
"""
|
||||
build status enumeration
|
||||
:cvar Unknown: build status is unknown
|
||||
:cvar Pending: package is out-of-dated and will be built soon
|
||||
:cvar Building: package is building right now
|
||||
:cvar Failed: package build failed
|
||||
:cvar Success: package has been built without errors
|
||||
"""
|
||||
|
||||
Unknown = "unknown"
|
||||
Pending = "pending"
|
||||
Building = "building"
|
||||
Failed = "failed"
|
||||
Success = "success"
|
||||
|
||||
def badges_color(self) -> str:
|
||||
"""
|
||||
convert itself to shield.io badges color
|
||||
:return: shields.io color
|
||||
"""
|
||||
if self == BuildStatusEnum.Pending:
|
||||
return "yellow"
|
||||
if self == BuildStatusEnum.Building:
|
||||
return "yellow"
|
||||
if self == BuildStatusEnum.Failed:
|
||||
return "critical"
|
||||
if self == BuildStatusEnum.Success:
|
||||
return "success"
|
||||
return "inactive"
|
||||
|
||||
|
||||
class BuildStatus:
|
||||
"""
|
||||
build status holder
|
||||
:ivar status: build status
|
||||
:ivar _timestamp: build status update time
|
||||
"""
|
||||
|
||||
def __init__(self, status: Union[BuildStatusEnum, str, None] = None,
|
||||
timestamp: Optional[datetime.datetime] = None) -> None:
|
||||
timestamp: Optional[int] = None) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param status: current build status if known. `BuildStatusEnum.Unknown` will be used if not set
|
||||
:param timestamp: build status timestamp. Current timestamp will be used if not set
|
||||
"""
|
||||
self.status = BuildStatusEnum(status) if status else BuildStatusEnum.Unknown
|
||||
self._timestamp = timestamp or datetime.datetime.utcnow()
|
||||
self.timestamp = timestamp or int(datetime.datetime.utcnow().timestamp())
|
||||
|
||||
@property
|
||||
def timestamp(self) -> str:
|
||||
return self._timestamp.strftime('%Y-%m-%d %H:%M:%S')
|
||||
@classmethod
|
||||
def from_json(cls: Type[BuildStatus], dump: Dict[str, Any]) -> BuildStatus:
|
||||
"""
|
||||
construct status properties from json dump
|
||||
:param dump: json dump body
|
||||
:return: status properties
|
||||
"""
|
||||
return cls(dump.get("status"), dump.get("timestamp"))
|
||||
|
||||
def pretty_print(self) -> str:
|
||||
"""
|
||||
generate pretty string representation
|
||||
:return: print-friendly string
|
||||
"""
|
||||
return f"{self.status.value} ({pretty_datetime(self.timestamp)})"
|
||||
|
||||
def view(self) -> Dict[str, Any]:
|
||||
"""
|
||||
generate json status view
|
||||
:return: json-friendly dictionary
|
||||
"""
|
||||
return {
|
||||
"status": self.status.value,
|
||||
"timestamp": self.timestamp
|
||||
}
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""
|
||||
compare object to other
|
||||
:param other: other object to compare
|
||||
:return: True in case if objects are equal
|
||||
"""
|
||||
if not isinstance(other, BuildStatus):
|
||||
return False
|
||||
return self.status == other.status and self.timestamp == other.timestamp
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
generate string representation of object
|
||||
:return: unique string representation
|
||||
"""
|
||||
return f"BuildStatus(status={self.status.value}, timestamp={self.timestamp})"
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -19,118 +19,247 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import aur
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import aur # type: ignore
|
||||
import logging
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pyalpm import Handle
|
||||
from srcinfo.parse import parse_srcinfo
|
||||
from typing import List, Set, Type
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
from pyalpm import vercmp # type: ignore
|
||||
from srcinfo.parse import parse_srcinfo # type: ignore
|
||||
from typing import Any, Dict, List, Optional, Set, Type, Union
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.exceptions import InvalidPackageInfo
|
||||
from ahriman.core.util import check_output
|
||||
from ahriman.models.package_desciption import PackageDescription
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
@dataclass
|
||||
class Package:
|
||||
"""
|
||||
package properties representation
|
||||
:ivar aur_url: AUR root url
|
||||
:ivar base: package base name
|
||||
:ivar packages: map of package names to their properties. Filled only on load from archive
|
||||
:ivar version: package full version
|
||||
"""
|
||||
|
||||
base: str
|
||||
version: str
|
||||
aur_url: str
|
||||
packages: Set[str] = field(default_factory=set)
|
||||
packages: Dict[str, PackageDescription]
|
||||
|
||||
_check_output = check_output
|
||||
|
||||
@property
|
||||
def git_url(self) -> str:
|
||||
return f'{self.aur_url}/{self.base}.git'
|
||||
"""
|
||||
:return: package git url to clone
|
||||
"""
|
||||
return f"{self.aur_url}/{self.base}.git"
|
||||
|
||||
@property
|
||||
def is_single_package(self) -> bool:
|
||||
"""
|
||||
:return: true in case if this base has only one package with the same name
|
||||
"""
|
||||
return self.base in self.packages and len(self.packages) == 1
|
||||
|
||||
@property
|
||||
def is_vcs(self) -> bool:
|
||||
return self.base.endswith('-bzr') \
|
||||
or self.base.endswith('-csv')\
|
||||
or self.base.endswith('-darcs')\
|
||||
or self.base.endswith('-git')\
|
||||
or self.base.endswith('-hg')\
|
||||
or self.base.endswith('-svn')
|
||||
"""
|
||||
:return: True in case if package base looks like VCS package and false otherwise
|
||||
"""
|
||||
return self.base.endswith("-bzr") \
|
||||
or self.base.endswith("-csv")\
|
||||
or self.base.endswith("-darcs")\
|
||||
or self.base.endswith("-git")\
|
||||
or self.base.endswith("-hg")\
|
||||
or self.base.endswith("-svn")
|
||||
|
||||
@property
|
||||
def web_url(self) -> str:
|
||||
return f'{self.aur_url}/packages/{self.base}'
|
||||
|
||||
# additional method to handle vcs versions
|
||||
def actual_version(self) -> str:
|
||||
if not self.is_vcs:
|
||||
return self.version
|
||||
|
||||
from ahriman.core.build_tools.task import Task
|
||||
clone_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
Task.fetch(clone_dir, self.git_url)
|
||||
# update pkgver first
|
||||
check_output('makepkg', '--nodeps', '--noprepare', '--nobuild',
|
||||
exception=None, cwd=clone_dir)
|
||||
# generate new .SRCINFO and put it to parser
|
||||
src_info_source = check_output('makepkg', '--printsrcinfo',
|
||||
exception=None, cwd=clone_dir)
|
||||
src_info, errors = parse_srcinfo(src_info_source)
|
||||
if errors:
|
||||
raise InvalidPackageInfo(errors)
|
||||
return f'{src_info["pkgver"]}-{src_info["pkgrel"]}'
|
||||
finally:
|
||||
shutil.rmtree(clone_dir, ignore_errors=True)
|
||||
"""
|
||||
:return: package AUR url
|
||||
"""
|
||||
return f"{self.aur_url}/packages/{self.base}"
|
||||
|
||||
@classmethod
|
||||
def from_archive(cls: Type[Package], path: str, pacman: Pacman, aur_url: str) -> Package:
|
||||
package = pacman.handle.load_pkg(path)
|
||||
return cls(package.base, package.version, aur_url, {package.name})
|
||||
def from_archive(cls: Type[Package], path: Path, pacman: Pacman, aur_url: str) -> Package:
|
||||
"""
|
||||
construct package properties from package archive
|
||||
:param path: path to package archive
|
||||
:param pacman: alpm wrapper instance
|
||||
:param aur_url: AUR root url
|
||||
:return: package properties
|
||||
"""
|
||||
package = pacman.handle.load_pkg(str(path))
|
||||
properties = PackageDescription(package.size, package.builddate, path.name, package.isize)
|
||||
return cls(package.base, package.version, aur_url, {package.name: properties})
|
||||
|
||||
@classmethod
|
||||
def from_aur(cls: Type[Package], name: str, aur_url: str)-> Package:
|
||||
def from_aur(cls: Type[Package], name: str, aur_url: str) -> Package:
|
||||
"""
|
||||
construct package properties from AUR page
|
||||
:param name: package name (either base or normal name)
|
||||
:param aur_url: AUR root url
|
||||
:return: package properties
|
||||
"""
|
||||
package = aur.info(name)
|
||||
return cls(package.package_base, package.version, aur_url, {package.name})
|
||||
return cls(package.package_base, package.version, aur_url, {package.name: PackageDescription()})
|
||||
|
||||
@classmethod
|
||||
def from_build(cls: Type[Package], path: str, aur_url: str) -> Package:
|
||||
with open(os.path.join(path, '.SRCINFO')) as fn:
|
||||
src_info, errors = parse_srcinfo(fn.read())
|
||||
def from_build(cls: Type[Package], path: Path, aur_url: str) -> Package:
|
||||
"""
|
||||
construct package properties from sources directory
|
||||
:param path: path to package sources directory
|
||||
:param aur_url: AUR root url
|
||||
:return: package properties
|
||||
"""
|
||||
srcinfo, errors = parse_srcinfo((path / ".SRCINFO").read_text())
|
||||
if errors:
|
||||
raise InvalidPackageInfo(errors)
|
||||
packages = set(src_info['packages'].keys())
|
||||
packages = {key: PackageDescription() for key in srcinfo["packages"]}
|
||||
version = cls.full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"])
|
||||
|
||||
return cls(src_info['pkgbase'], f'{src_info["pkgver"]}-{src_info["pkgrel"]}', aur_url, packages)
|
||||
return cls(srcinfo["pkgbase"], version, aur_url, packages)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Package], dump: Dict[str, Any]) -> Package:
|
||||
"""
|
||||
construct package properties from json dump
|
||||
:param dump: json dump body
|
||||
:return: package properties
|
||||
"""
|
||||
packages = {
|
||||
key: PackageDescription(**value)
|
||||
for key, value in dump.get("packages", {}).items()
|
||||
}
|
||||
return Package(
|
||||
base=dump["base"],
|
||||
version=dump["version"],
|
||||
aur_url=dump["aur_url"],
|
||||
packages=packages)
|
||||
|
||||
@staticmethod
|
||||
def dependencies(path: str) -> Set[str]:
|
||||
with open(os.path.join(path, '.SRCINFO')) as fn:
|
||||
src_info, errors = parse_srcinfo(fn.read())
|
||||
def dependencies(path: Path) -> Set[str]:
|
||||
"""
|
||||
load dependencies from package sources
|
||||
:param path: path to package sources directory
|
||||
:return: list of package dependencies including makedepends array, but excluding packages from this base
|
||||
"""
|
||||
# additional function to remove versions from dependencies
|
||||
def trim_version(name: str) -> str:
|
||||
for symbol in ("<", "=", ">"):
|
||||
name = name.split(symbol)[0]
|
||||
return name
|
||||
|
||||
srcinfo, errors = parse_srcinfo((path / ".SRCINFO").read_text())
|
||||
if errors:
|
||||
raise InvalidPackageInfo(errors)
|
||||
makedepends = src_info['makedepends']
|
||||
makedepends = srcinfo.get("makedepends", [])
|
||||
# sum over each package
|
||||
depends: List[str] = src_info.get('depends', [])
|
||||
for package in src_info['packages'].values():
|
||||
depends.extend(package.get('depends', []))
|
||||
depends: List[str] = srcinfo.get("depends", [])
|
||||
for package in srcinfo["packages"].values():
|
||||
depends.extend(package.get("depends", []))
|
||||
# we are not interested in dependencies inside pkgbase
|
||||
packages = set(src_info['packages'].keys())
|
||||
return set(depends + makedepends) - packages
|
||||
packages = set(srcinfo["packages"].keys())
|
||||
full_list = set(depends + makedepends) - packages
|
||||
return {trim_version(package_name) for package_name in full_list}
|
||||
|
||||
@staticmethod
|
||||
def load(path: str, pacman: Pacman, aur_url: str) -> Package:
|
||||
def full_version(epoch: Optional[str], pkgver: str, pkgrel: str) -> str:
|
||||
"""
|
||||
generate full version from components
|
||||
:param epoch: package epoch if any
|
||||
:param pkgver: package version
|
||||
:param pkgrel: package release version (archlinux specific)
|
||||
:return: generated version
|
||||
"""
|
||||
prefix = f"{epoch}:" if epoch else ""
|
||||
return f"{prefix}{pkgver}-{pkgrel}"
|
||||
|
||||
@staticmethod
|
||||
def load(path: Union[Path, str], pacman: Pacman, aur_url: str) -> Package:
|
||||
"""
|
||||
package constructor from available sources
|
||||
:param path: one of path to sources directory, path to archive or package name/base
|
||||
:param pacman: alpm wrapper instance (required to load from archive)
|
||||
:param aur_url: AUR root url
|
||||
:return: package properties
|
||||
"""
|
||||
try:
|
||||
if os.path.isdir(path):
|
||||
package: Package = Package.from_build(path, aur_url)
|
||||
elif os.path.exists(path):
|
||||
package = Package.from_archive(path, pacman, aur_url)
|
||||
maybe_path = Path(path)
|
||||
if maybe_path.is_dir():
|
||||
package: Package = Package.from_build(maybe_path, aur_url)
|
||||
elif maybe_path.is_file():
|
||||
package = Package.from_archive(maybe_path, pacman, aur_url)
|
||||
else:
|
||||
package = Package.from_aur(path, aur_url)
|
||||
package = Package.from_aur(str(path), aur_url)
|
||||
return package
|
||||
except InvalidPackageInfo:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise InvalidPackageInfo(str(e))
|
||||
|
||||
def is_outdated(self, remote: Package) -> bool:
|
||||
remote_version = remote.actual_version() # either normal version or updated VCS
|
||||
result = check_output('vercmp', self.version, remote_version, exception=None)
|
||||
return True if int(result) < 0 else False
|
||||
def actual_version(self, paths: RepositoryPaths) -> str:
|
||||
"""
|
||||
additional method to handle VCS package versions
|
||||
:param paths: repository paths instance
|
||||
:return: package version if package is not VCS and current version according to VCS otherwise
|
||||
"""
|
||||
if not self.is_vcs:
|
||||
return self.version
|
||||
|
||||
from ahriman.core.build_tools.task import Task
|
||||
|
||||
clone_dir = paths.cache / self.base
|
||||
logger = logging.getLogger("build_details")
|
||||
Task.fetch(clone_dir, self.git_url)
|
||||
|
||||
try:
|
||||
# update pkgver first
|
||||
Package._check_output("makepkg", "--nodeps", "--nobuild", exception=None, cwd=clone_dir, logger=logger)
|
||||
# generate new .SRCINFO and put it to parser
|
||||
srcinfo_source = Package._check_output(
|
||||
"makepkg",
|
||||
"--printsrcinfo",
|
||||
exception=None,
|
||||
cwd=clone_dir,
|
||||
logger=logger)
|
||||
srcinfo, errors = parse_srcinfo(srcinfo_source)
|
||||
if errors:
|
||||
raise InvalidPackageInfo(errors)
|
||||
|
||||
return self.full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"])
|
||||
except Exception:
|
||||
logger.exception("cannot determine version of VCS package, make sure that you have VCS tools installed")
|
||||
|
||||
return self.version
|
||||
|
||||
def is_outdated(self, remote: Package, paths: RepositoryPaths) -> bool:
|
||||
"""
|
||||
check if package is out-of-dated
|
||||
:param remote: package properties from remote source
|
||||
:param paths: repository paths instance. Required for VCS packages cache
|
||||
:return: True if the package is out-of-dated and False otherwise
|
||||
"""
|
||||
remote_version = remote.actual_version(paths) # either normal version or updated VCS
|
||||
result: int = vercmp(self.version, remote_version)
|
||||
return result < 0
|
||||
|
||||
def pretty_print(self) -> str:
|
||||
"""
|
||||
generate pretty string representation
|
||||
:return: print-friendly string
|
||||
"""
|
||||
details = "" if self.is_single_package else f""" ({" ".join(sorted(self.packages.keys()))})"""
|
||||
return f"{self.base}{details}"
|
||||
|
||||
def view(self) -> Dict[str, Any]:
|
||||
"""
|
||||
generate json package view
|
||||
:return: json-friendly dictionary
|
||||
"""
|
||||
return asdict(self)
|
||||
|
45
src/ahriman/models/package_desciption.py
Normal file
45
src/ahriman/models/package_desciption.py
Normal file
@ -0,0 +1,45 @@
|
||||
#
|
||||
# Copyright (c) 2021 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
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class PackageDescription:
|
||||
"""
|
||||
package specific properties
|
||||
:ivar archive_size: package archive size
|
||||
:ivar build_date: package build date
|
||||
:ivar filename: package archive name
|
||||
:ivar installed_size: package installed size
|
||||
"""
|
||||
|
||||
archive_size: Optional[int] = None
|
||||
build_date: Optional[int] = None
|
||||
filename: Optional[str] = None
|
||||
installed_size: Optional[int] = None
|
||||
|
||||
@property
|
||||
def filepath(self) -> Optional[Path]:
|
||||
"""
|
||||
:return: path object for current filename
|
||||
"""
|
||||
return Path(self.filename) if self.filename is not None else None
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -25,10 +25,20 @@ from ahriman.core.exceptions import InvalidOption
|
||||
|
||||
|
||||
class ReportSettings(Enum):
|
||||
"""
|
||||
report targets enumeration
|
||||
:cvar HTML: html report generation
|
||||
"""
|
||||
|
||||
HTML = auto()
|
||||
|
||||
@staticmethod
|
||||
def from_option(value: str) -> ReportSettings:
|
||||
if value.lower() in ('html',):
|
||||
"""
|
||||
construct value from configuration
|
||||
:param value: configuration value
|
||||
:return: parsed value
|
||||
"""
|
||||
if value.lower() in ("html",):
|
||||
return ReportSettings.HTML
|
||||
raise InvalidOption(value)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -17,54 +17,72 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class RepositoryPaths:
|
||||
root: str
|
||||
"""
|
||||
repository paths holder. For the most operations with paths you want to use this object
|
||||
:ivar root: repository root (i.e. ahriman home)
|
||||
:ivar architecture: repository architecture
|
||||
"""
|
||||
|
||||
root: Path
|
||||
architecture: str
|
||||
|
||||
@property
|
||||
def chroot(self) -> str:
|
||||
'''
|
||||
def cache(self) -> Path:
|
||||
"""
|
||||
:return: directory for packages cache (mainly used for VCS packages)
|
||||
"""
|
||||
return self.root / "cache"
|
||||
|
||||
@property
|
||||
def chroot(self) -> Path:
|
||||
"""
|
||||
:return: directory for devtools chroot
|
||||
'''
|
||||
return os.path.join(self.root, 'chroot')
|
||||
"""
|
||||
# for the chroot directory devtools will create own tree and we don"t have to specify architecture here
|
||||
return self.root / "chroot"
|
||||
|
||||
@property
|
||||
def manual(self) -> str:
|
||||
'''
|
||||
def manual(self) -> Path:
|
||||
"""
|
||||
:return: directory for manual updates (i.e. from add command)
|
||||
'''
|
||||
return os.path.join(self.root, 'manual')
|
||||
"""
|
||||
return self.root / "manual" / self.architecture
|
||||
|
||||
@property
|
||||
def packages(self) -> str:
|
||||
'''
|
||||
def packages(self) -> Path:
|
||||
"""
|
||||
:return: directory for built packages
|
||||
'''
|
||||
return os.path.join(self.root, 'packages')
|
||||
"""
|
||||
return self.root / "packages" / self.architecture
|
||||
|
||||
@property
|
||||
def repository(self) -> str:
|
||||
'''
|
||||
def repository(self) -> Path:
|
||||
"""
|
||||
:return: repository directory
|
||||
'''
|
||||
return os.path.join(self.root, 'repository', self.architecture)
|
||||
"""
|
||||
return self.root / "repository" / self.architecture
|
||||
|
||||
@property
|
||||
def sources(self) -> str:
|
||||
'''
|
||||
def sources(self) -> Path:
|
||||
"""
|
||||
:return: directory for downloaded PKGBUILDs for current build
|
||||
'''
|
||||
return os.path.join(self.root, 'sources')
|
||||
"""
|
||||
return self.root / "sources" / self.architecture
|
||||
|
||||
def create_tree(self) -> None:
|
||||
os.makedirs(self.chroot, mode=0o755, exist_ok=True)
|
||||
os.makedirs(self.manual, mode=0o755, exist_ok=True)
|
||||
os.makedirs(self.packages, mode=0o755, exist_ok=True)
|
||||
os.makedirs(self.repository, mode=0o755, exist_ok=True)
|
||||
os.makedirs(self.sources, mode=0o755, exist_ok=True)
|
||||
"""
|
||||
create ahriman working tree
|
||||
"""
|
||||
self.cache.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
self.chroot.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
self.manual.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
self.packages.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
self.repository.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
self.sources.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -25,13 +25,24 @@ from ahriman.core.exceptions import InvalidOption
|
||||
|
||||
|
||||
class SignSettings(Enum):
|
||||
"""
|
||||
sign targets enumeration
|
||||
:cvar SignPackages: sign each package
|
||||
:cvar SignRepository: sign repository database file
|
||||
"""
|
||||
|
||||
SignPackages = auto()
|
||||
SignRepository = auto()
|
||||
|
||||
@staticmethod
|
||||
def from_option(value: str) -> SignSettings:
|
||||
if value.lower() in ('package', 'packages', 'sign-package'):
|
||||
"""
|
||||
construct value from configuration
|
||||
:param value: configuration value
|
||||
:return: parsed value
|
||||
"""
|
||||
if value.lower() in ("package", "packages", "sign-package"):
|
||||
return SignSettings.SignPackages
|
||||
elif value.lower() in ('repository', 'sign-repository'):
|
||||
if value.lower() in ("repository", "sign-repository"):
|
||||
return SignSettings.SignRepository
|
||||
raise InvalidOption(value)
|
||||
raise InvalidOption(value)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -25,13 +25,24 @@ from ahriman.core.exceptions import InvalidOption
|
||||
|
||||
|
||||
class UploadSettings(Enum):
|
||||
"""
|
||||
remote synchronization targets enumeration
|
||||
:cvar Rsync: sync via rsync
|
||||
:cvar S3: sync to Amazon S3
|
||||
"""
|
||||
|
||||
Rsync = auto()
|
||||
S3 = auto()
|
||||
|
||||
@staticmethod
|
||||
def from_option(value: str) -> UploadSettings:
|
||||
if value.lower() in ('rsync',):
|
||||
"""
|
||||
construct value from configuration
|
||||
:param value: configuration value
|
||||
:return: parsed value
|
||||
"""
|
||||
if value.lower() in ("rsync",):
|
||||
return UploadSettings.Rsync
|
||||
elif value.lower() in ('s3',):
|
||||
if value.lower() in ("s3",):
|
||||
return UploadSettings.S3
|
||||
raise InvalidOption(value)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -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__ = '0.11.1'
|
||||
__version__ = "0.17.0"
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -17,21 +17,30 @@
|
||||
# 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 aiohttp.web import middleware, Request, Response
|
||||
from aiohttp.web import middleware, Request
|
||||
from aiohttp.web_exceptions import HTTPClientError
|
||||
from aiohttp.web_response import StreamResponse
|
||||
from logging import Logger
|
||||
from typing import Callable
|
||||
from typing import Awaitable, Callable
|
||||
|
||||
|
||||
def exception_handler(logger: Logger) -> Callable:
|
||||
HandlerType = Callable[[Request], Awaitable[StreamResponse]]
|
||||
|
||||
|
||||
def exception_handler(logger: Logger) -> Callable[[Request, HandlerType], Awaitable[StreamResponse]]:
|
||||
"""
|
||||
exception handler middleware. Just log any exception (except for client ones)
|
||||
:param logger: class logger
|
||||
:return: built middleware
|
||||
"""
|
||||
@middleware
|
||||
async def handle(request: Request, handler: Callable) -> Response:
|
||||
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
|
||||
try:
|
||||
return await handler(request)
|
||||
except HTTPClientError:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception(f'exception during performing request to {request.path}', exc_info=True)
|
||||
logger.exception(f"exception during performing request to {request.path}")
|
||||
raise
|
||||
|
||||
return handle
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -19,16 +19,42 @@
|
||||
#
|
||||
from aiohttp.web import Application
|
||||
|
||||
from ahriman.web.views.ahriman import AhrimanView
|
||||
from ahriman.web.views.index import IndexView
|
||||
from ahriman.web.views.package import PackageView
|
||||
from ahriman.web.views.packages import PackagesView
|
||||
|
||||
|
||||
def setup_routes(app: Application) -> None:
|
||||
app.router.add_get('/', IndexView)
|
||||
app.router.add_get('/index.html', IndexView)
|
||||
def setup_routes(application: Application) -> None:
|
||||
"""
|
||||
setup all defined routes
|
||||
|
||||
app.router.add_post('/api/v1/packages', PackagesView)
|
||||
Available routes are:
|
||||
|
||||
app.router.add_delete('/api/v1/packages/{package}', PackageView)
|
||||
app.router.add_post('/api/v1/packages/{package}', PackageView)
|
||||
GET / get build status page
|
||||
GET /index.html same as above
|
||||
|
||||
GET /api/v1/ahriman get current service status
|
||||
POST /api/v1/ahriman update service status
|
||||
|
||||
GET /api/v1/packages get all known packages
|
||||
POST /api/v1/packages force update every package from repository
|
||||
|
||||
DELETE /api/v1/package/:base delete package base from status page
|
||||
GET /api/v1/package/:base get package base status
|
||||
POST /api/v1/package/:base update package base status
|
||||
|
||||
:param application: web application instance
|
||||
"""
|
||||
application.router.add_get("/", IndexView)
|
||||
application.router.add_get("/index.html", IndexView)
|
||||
|
||||
application.router.add_get("/api/v1/ahriman", AhrimanView)
|
||||
application.router.add_post("/api/v1/ahriman", AhrimanView)
|
||||
|
||||
application.router.add_get("/api/v1/packages", PackagesView)
|
||||
application.router.add_post("/api/v1/packages", PackagesView)
|
||||
|
||||
application.router.add_delete("/api/v1/packages/{package}", PackageView)
|
||||
application.router.add_get("/api/v1/packages/{package}", PackageView)
|
||||
application.router.add_post("/api/v1/packages/{package}", PackageView)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
58
src/ahriman/web/views/ahriman.py
Normal file
58
src/ahriman/web/views/ahriman.py
Normal file
@ -0,0 +1,58 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
|
||||
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
class AhrimanView(BaseView):
|
||||
"""
|
||||
service status web view
|
||||
"""
|
||||
|
||||
async def get(self) -> Response:
|
||||
"""
|
||||
get current service status
|
||||
:return: 200 with service status object
|
||||
"""
|
||||
return json_response(self.service.status.view())
|
||||
|
||||
async def post(self) -> Response:
|
||||
"""
|
||||
update service status
|
||||
|
||||
JSON body must be supplied, the following model is used:
|
||||
{
|
||||
"status": "unknown", # service status string, must be valid `BuildStatusEnum`
|
||||
}
|
||||
|
||||
:return: 204 on success
|
||||
"""
|
||||
data = await self.request.json()
|
||||
|
||||
try:
|
||||
status = BuildStatusEnum(data["status"])
|
||||
except Exception as e:
|
||||
raise HTTPBadRequest(text=str(e))
|
||||
|
||||
self.service.update_self(status)
|
||||
|
||||
return HTTPNoContent()
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -19,12 +19,18 @@
|
||||
#
|
||||
from aiohttp.web import View
|
||||
|
||||
from ahriman.core.watcher.watcher import Watcher
|
||||
from ahriman.core.status.watcher import Watcher
|
||||
|
||||
|
||||
# special class to make it typed
|
||||
class BaseView(View):
|
||||
"""
|
||||
base web view to make things typed
|
||||
"""
|
||||
|
||||
@property
|
||||
def service(self) -> Watcher:
|
||||
return self.request.app['watcher']
|
||||
"""
|
||||
:return: build status watcher instance
|
||||
"""
|
||||
watcher: Watcher = self.request.app["watcher"]
|
||||
return watcher
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -17,33 +17,57 @@
|
||||
# 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 aiohttp_jinja2 import template
|
||||
import aiohttp_jinja2
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
import ahriman.version as version
|
||||
|
||||
from ahriman.core.util import pretty_datetime
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
class IndexView(BaseView):
|
||||
"""
|
||||
root view
|
||||
|
||||
@template("build-status.jinja2")
|
||||
It uses jinja2 templates for report generation, the following variables are allowed:
|
||||
|
||||
architecture - repository architecture, string, required
|
||||
packages - sorted list of packages properties: base, packages (sorted list), status,
|
||||
timestamp, version, web_url. Required
|
||||
repository - repository name, string, required
|
||||
service - service status properties: status, status_color, timestamp. Required
|
||||
version - ahriman version, string, required
|
||||
"""
|
||||
|
||||
@aiohttp_jinja2.template("build-status.jinja2")
|
||||
async def get(self) -> Dict[str, Any]:
|
||||
"""
|
||||
process get request. No parameters supported here
|
||||
:return: parameters for jinja template
|
||||
"""
|
||||
# some magic to make it jinja-friendly
|
||||
packages = [
|
||||
{
|
||||
'base': package.base,
|
||||
'packages': [p for p in sorted(package.packages)],
|
||||
'status': status.status.value,
|
||||
'timestamp': status.timestamp,
|
||||
'version': package.version,
|
||||
'web_url': package.web_url
|
||||
"base": package.base,
|
||||
"packages": list(sorted(package.packages)),
|
||||
"status": status.status.value,
|
||||
"timestamp": pretty_datetime(status.timestamp),
|
||||
"version": package.version,
|
||||
"web_url": package.web_url
|
||||
} for package, status in sorted(self.service.packages, key=lambda item: item[0].base)
|
||||
]
|
||||
service = {
|
||||
"status": self.service.status.status.value,
|
||||
"status_color": self.service.status.status.badges_color(),
|
||||
"timestamp": pretty_datetime(self.service.status.timestamp)
|
||||
}
|
||||
|
||||
return {
|
||||
'architecture': self.service.architecture,
|
||||
'packages': packages,
|
||||
'repository': self.service.repository.name,
|
||||
'version': version.__version__,
|
||||
}
|
||||
"architecture": self.service.architecture,
|
||||
"packages": packages,
|
||||
"repository": self.service.repository.name,
|
||||
"service": service,
|
||||
"version": version.__version__,
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -17,27 +17,74 @@
|
||||
# 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 aiohttp.web import HTTPOk, Response
|
||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
|
||||
|
||||
from ahriman.core.exceptions import UnknownPackage
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
class PackageView(BaseView):
|
||||
"""
|
||||
package base specific web view
|
||||
"""
|
||||
|
||||
async def get(self) -> Response:
|
||||
"""
|
||||
get current package base status
|
||||
:return: 200 with package description on success
|
||||
"""
|
||||
base = self.request.match_info["package"]
|
||||
|
||||
try:
|
||||
package, status = self.service.get(base)
|
||||
except UnknownPackage:
|
||||
raise HTTPNotFound()
|
||||
|
||||
response = [
|
||||
{
|
||||
"package": package.view(),
|
||||
"status": status.view()
|
||||
}
|
||||
]
|
||||
return json_response(response)
|
||||
|
||||
async def delete(self) -> Response:
|
||||
base = self.request.match_info['package']
|
||||
"""
|
||||
delete package base from status page
|
||||
:return: 204 on success
|
||||
"""
|
||||
base = self.request.match_info["package"]
|
||||
self.service.remove(base)
|
||||
|
||||
return HTTPOk()
|
||||
return HTTPNoContent()
|
||||
|
||||
async def post(self) -> Response:
|
||||
base = self.request.match_info['package']
|
||||
"""
|
||||
update package build status
|
||||
|
||||
JSON body must be supplied, the following model is used:
|
||||
{
|
||||
"status": "unknown", # package build status string, must be valid `BuildStatusEnum`
|
||||
"package": {} # package body (use `dataclasses.asdict` to generate one), optional.
|
||||
# Must be supplied in case if package base is unknown
|
||||
}
|
||||
|
||||
:return: 204 on success
|
||||
"""
|
||||
base = self.request.match_info["package"]
|
||||
data = await self.request.json()
|
||||
|
||||
package = Package(**data['package']) if 'package' in data else None
|
||||
status = BuildStatusEnum(data['status'])
|
||||
self.service.update(base, status, package)
|
||||
try:
|
||||
package = Package.from_json(data["package"]) if "package" in data else None
|
||||
status = BuildStatusEnum(data["status"])
|
||||
except Exception as e:
|
||||
raise HTTPBadRequest(text=str(e))
|
||||
|
||||
return HTTPOk()
|
||||
try:
|
||||
self.service.update(base, status, package)
|
||||
except UnknownPackage:
|
||||
raise HTTPBadRequest(text=f"Package {base} is unknown, but no package body set")
|
||||
|
||||
return HTTPNoContent()
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -17,14 +17,34 @@
|
||||
# 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 aiohttp.web import HTTPOk, Response
|
||||
from aiohttp.web import HTTPNoContent, Response, json_response
|
||||
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
class PackagesView(BaseView):
|
||||
"""
|
||||
global watcher view
|
||||
"""
|
||||
|
||||
async def get(self) -> Response:
|
||||
"""
|
||||
get current packages status
|
||||
:return: 200 with package description on success
|
||||
"""
|
||||
response = [
|
||||
{
|
||||
"package": package.view(),
|
||||
"status": status.view()
|
||||
} for package, status in self.service.packages
|
||||
]
|
||||
return json_response(response)
|
||||
|
||||
async def post(self) -> Response:
|
||||
"""
|
||||
reload all packages from repository. No parameters supported here
|
||||
:return: 204 on success
|
||||
"""
|
||||
self.service.load()
|
||||
|
||||
return HTTPOk()
|
||||
return HTTPNoContent()
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Copyright (c) 2021 Evgenii Alekseev.
|
||||
# Copyright (c) 2021 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -25,51 +25,72 @@ from aiohttp import web
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import InitializeException
|
||||
from ahriman.core.watcher.watcher import Watcher
|
||||
from ahriman.core.status.watcher import Watcher
|
||||
from ahriman.web.middlewares.exception_handler import exception_handler
|
||||
from ahriman.web.routes import setup_routes
|
||||
|
||||
|
||||
async def on_shutdown(app: web.Application) -> None:
|
||||
app.logger.warning('server terminated')
|
||||
async def on_shutdown(application: web.Application) -> None:
|
||||
"""
|
||||
web application shutdown handler
|
||||
:param application: web application instance
|
||||
"""
|
||||
application.logger.warning("server terminated")
|
||||
|
||||
|
||||
async def on_startup(app: web.Application) -> None:
|
||||
app.logger.info('server started')
|
||||
async def on_startup(application: web.Application) -> None:
|
||||
"""
|
||||
web application start handler
|
||||
:param application: web application instance
|
||||
"""
|
||||
application.logger.info("server started")
|
||||
try:
|
||||
app['watcher'].load()
|
||||
application["watcher"].load()
|
||||
except Exception:
|
||||
app.logger.exception('could not load packages', exc_info=True)
|
||||
application.logger.exception("could not load packages")
|
||||
raise InitializeException()
|
||||
|
||||
|
||||
def run_server(app: web.Application, architecture: str) -> None:
|
||||
app.logger.info('start server')
|
||||
def run_server(application: web.Application) -> None:
|
||||
"""
|
||||
run web application
|
||||
:param application: web application instance
|
||||
"""
|
||||
application.logger.info("start server")
|
||||
|
||||
section = app['config'].get_section_name('web', architecture)
|
||||
host = app['config'].get(section, 'host')
|
||||
port = app['config'].getint(section, 'port')
|
||||
section = application["config"].get_section_name("web", application["architecture"])
|
||||
host = application["config"].get(section, "host")
|
||||
port = application["config"].getint(section, "port")
|
||||
|
||||
web.run_app(app, host=host, port=port, handle_signals=False)
|
||||
web.run_app(application, host=host, port=port, handle_signals=False,
|
||||
access_log=logging.getLogger("http"))
|
||||
|
||||
|
||||
def setup_service(architecture: str, config: Configuration) -> web.Application:
|
||||
app = web.Application(logger=logging.getLogger('http'))
|
||||
app.on_shutdown.append(on_shutdown)
|
||||
app.on_startup.append(on_startup)
|
||||
"""
|
||||
create web application
|
||||
:param architecture: repository architecture
|
||||
:param config: configuration instance
|
||||
:return: web application instance
|
||||
"""
|
||||
application = web.Application(logger=logging.getLogger("http"))
|
||||
application.on_shutdown.append(on_shutdown)
|
||||
application.on_startup.append(on_startup)
|
||||
|
||||
app.middlewares.append(web.normalize_path_middleware(append_slash=False, remove_slash=True))
|
||||
app.middlewares.append(exception_handler(app.logger))
|
||||
application.middlewares.append(web.normalize_path_middleware(append_slash=False, remove_slash=True))
|
||||
application.middlewares.append(exception_handler(application.logger))
|
||||
|
||||
app.logger.info('setup routes')
|
||||
setup_routes(app)
|
||||
app.logger.info('setup templates')
|
||||
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(config.get('web', 'templates')))
|
||||
application.logger.info("setup routes")
|
||||
setup_routes(application)
|
||||
|
||||
app.logger.info('setup configuration')
|
||||
app['config'] = config
|
||||
application.logger.info("setup templates")
|
||||
aiohttp_jinja2.setup(application, loader=jinja2.FileSystemLoader(config.getpath("web", "templates")))
|
||||
|
||||
app.logger.info('setup watcher')
|
||||
app['watcher'] = Watcher(architecture, config)
|
||||
application.logger.info("setup configuration")
|
||||
application["config"] = config
|
||||
application["architecture"] = architecture
|
||||
|
||||
return app
|
||||
application.logger.info("setup watcher")
|
||||
application["watcher"] = Watcher(architecture, config)
|
||||
|
||||
return application
|
||||
|
30
tests/ahriman/application/conftest.py
Normal file
30
tests/ahriman/application/conftest.py
Normal file
@ -0,0 +1,30 @@
|
||||
import argparse
|
||||
import pytest
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.ahriman import _parser
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.lock import Lock
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def application(configuration: Configuration, mocker: MockerFixture) -> Application:
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
return Application("x86_64", configuration)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def args() -> argparse.Namespace:
|
||||
return argparse.Namespace(lock=None, force=False, unsafe=False, no_report=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lock(args: argparse.Namespace, configuration: Configuration) -> Lock:
|
||||
return Lock(args, "x86_64", configuration)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
return _parser()
|
27
tests/ahriman/application/handlers/test_handler.py
Normal file
27
tests/ahriman/application/handlers/test_handler.py
Normal file
@ -0,0 +1,27 @@
|
||||
import argparse
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def test_call(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must call inside lock
|
||||
"""
|
||||
mocker.patch("ahriman.application.handlers.Handler.run")
|
||||
enter_mock = mocker.patch("ahriman.application.lock.Lock.__enter__")
|
||||
exit_mock = mocker.patch("ahriman.application.lock.Lock.__exit__")
|
||||
|
||||
assert Handler._call(args, "x86_64", configuration)
|
||||
enter_mock.assert_called_once()
|
||||
exit_mock.assert_called_once()
|
||||
|
||||
|
||||
def test_call_exception(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must process exception
|
||||
"""
|
||||
mocker.patch("ahriman.application.lock.Lock.__enter__", side_effect=Exception())
|
||||
assert not Handler._call(args, "x86_64", configuration)
|
24
tests/ahriman/application/handlers/test_handler_add.py
Normal file
24
tests/ahriman/application/handlers/test_handler_add.py
Normal file
@ -0,0 +1,24 @@
|
||||
import argparse
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers import Add
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
args.package = []
|
||||
args.without_dependencies = False
|
||||
return args
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
args = _default_args(args)
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
application_mock = mocker.patch("ahriman.application.application.Application.add")
|
||||
|
||||
Add.run(args, "x86_64", configuration)
|
||||
application_mock.assert_called_once()
|
27
tests/ahriman/application/handlers/test_handler_clean.py
Normal file
27
tests/ahriman/application/handlers/test_handler_clean.py
Normal file
@ -0,0 +1,27 @@
|
||||
import argparse
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers import Clean
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
args.no_build = False
|
||||
args.no_cache = False
|
||||
args.no_chroot = False
|
||||
args.no_manual = False
|
||||
args.no_packages = False
|
||||
return args
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
args = _default_args(args)
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
application_mock = mocker.patch("ahriman.application.application.Application.clean")
|
||||
|
||||
Clean.run(args, "x86_64", configuration)
|
||||
application_mock.assert_called_once()
|
17
tests/ahriman/application/handlers/test_handler_dump.py
Normal file
17
tests/ahriman/application/handlers/test_handler_dump.py
Normal file
@ -0,0 +1,17 @@
|
||||
import argparse
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers import Dump
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
application_mock = mocker.patch("ahriman.core.configuration.Configuration.dump")
|
||||
|
||||
Dump.run(args, "x86_64", configuration)
|
||||
application_mock.assert_called_once()
|
19
tests/ahriman/application/handlers/test_handler_rebuild.py
Normal file
19
tests/ahriman/application/handlers/test_handler_rebuild.py
Normal file
@ -0,0 +1,19 @@
|
||||
import argparse
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers import Rebuild
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages")
|
||||
application_mock = mocker.patch("ahriman.application.application.Application.update")
|
||||
|
||||
Rebuild.run(args, "x86_64", configuration)
|
||||
application_packages_mock.assert_called_once()
|
||||
application_mock.assert_called_once()
|
23
tests/ahriman/application/handlers/test_handler_remove.py
Normal file
23
tests/ahriman/application/handlers/test_handler_remove.py
Normal file
@ -0,0 +1,23 @@
|
||||
import argparse
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers import Remove
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
args.package = []
|
||||
return args
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
args = _default_args(args)
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
application_mock = mocker.patch("ahriman.application.application.Application.remove")
|
||||
|
||||
Remove.run(args, "x86_64", configuration)
|
||||
application_mock.assert_called_once()
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user