Compare commits

..

No commits in common. "debian/master" and "upstream" have entirely different histories.

64 changed files with 1365 additions and 4581 deletions

View file

@ -1,24 +0,0 @@
---
name: Disperse configuration
"on":
- push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
- name: Install dependencies
run: |
sudo apt install protobuf-compiler
- name: Install disperse
run: |
pip install git+https://github.com/jelmer/disperse
- name: Validate disperse.conf
run: |
PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python disperse validate .

View file

@ -1,11 +1,6 @@
---
name: Python package
"on":
push:
pull_request:
schedule:
- cron: '0 6 * * *' # Daily 6AM UTC build
on: [push, pull_request]
jobs:
build:
@ -14,7 +9,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
python-version: [3.7, 3.8, 3.9, '3.10']
python-version: [3.7, 3.8]
fail-fast: false
steps:
@ -25,28 +20,28 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[remote,dep_server,dev]"
python -m pip install --upgrade pip flake8 cython
python setup.py develop
- name: Install Debian-specific dependencies
run: |
sudo apt update
sudo apt install python3-wheel libapt-pkg-dev
python -m pip install \
python_apt@git+https://salsa.debian.org/apt-team/python-apt.git
sudo apt install libapt-pkg-dev
python -m pip install wheel
python -m pip install git+https://salsa.debian.org/apt-team/python-apt
python -m pip install -e ".[debian]"
python -m pip install testtools
mkdir -p ~/.config/breezy/plugins
brz branch lp:brz-debian ~/.config/breezy/plugins/debian
if: "matrix.python-version != 'pypy3' && matrix.os == 'ubuntu-latest'"
- name: Style checks
run: |
pip install flake8
python -m flake8
- name: Typing checks
run: |
pip install -U mypy types-toml
pip install -U mypy
python -m mypy ognibuild
if: "matrix.python-version != 'pypy3'"
- name: Test suite run
run: |
python -m unittest tests.test_suite
python -m unittest ognibuild.tests.test_suite
env:
PYTHONHASHSEED: random

1
.gitignore vendored
View file

@ -1,4 +1,3 @@
.coverage
build
*~
ognibuild.egg-info

View file

@ -1,20 +0,0 @@
check:: style
style:
flake8
check:: testsuite
testsuite:
python3 -m unittest tests.test_suite
check:: typing
typing:
mypy ognibuild tests
coverage:
python3 -m coverage run -m unittest tests.test_suite
coverage-html:
python3 -m coverage html

17
PKG-INFO Normal file
View file

@ -0,0 +1,17 @@
Metadata-Version: 2.1
Name: ognibuild
Version: 0.0.7
Summary: Detect and run any build system
Home-page: https://jelmer.uk/code/ognibuild
Maintainer: Jelmer Vernooij
Maintainer-email: jelmer@jelmer.uk
License: GNU GPLv2 or later
Description: UNKNOWN
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Operating System :: POSIX
Provides-Extra: debian

View file

@ -31,12 +31,6 @@ Ognibuild has a number of subcommands:
It also includes a subcommand that can fix up the build dependencies
for Debian packages, called deb-fix-build.
### Examples
```
ogni -d https://gitlab.gnome.org/GNOME/fractal install
```
## Status
Ognibuild is functional, but sometimes rough around the edges. If you run into

62
debian/changelog vendored
View file

@ -1,62 +0,0 @@
ognibuild (0.0.15-0.1) UNRELEASED; urgency=low
* Non-maintainer upload.
* New upstream release.
* Bump debhelper from old 12 to 13.
* Update standards version to 4.6.1, no changes needed.
-- Tianyu Chen <billchenchina2001@gmail.com> Tue, 22 Nov 2022 11:12:48 +0800
ognibuild (0.0.7-1) UNRELEASED; urgency=low
* New upstream release.
-- Debian Janitor <janitor@jelmer.uk> Wed, 02 Jun 2021 15:12:48 -0000
ognibuild (0.0.6+git20210517.1.8189e91-1) unstable; urgency=low
* New upstream snapshot.
-- Jelmer Vernooij <jelmer@debian.org> Tue, 18 May 2021 20:53:15 +0100
ognibuild (0.0.5-1) unstable; urgency=low
* New upstream release.
+ Fixes cmake support. Closes: #988572
+ Preserve environment when building Python packages. Closes: #988571
-- Jelmer Vernooij <jelmer@debian.org> Tue, 18 May 2021 01:34:11 +0100
ognibuild (0.0.4-1) unstable; urgency=low
* New upstream release.
-- Jelmer Vernooij <jelmer@debian.org> Wed, 07 Apr 2021 00:11:09 +0100
ognibuild (0.0.3-1) unstable; urgency=medium
* Add missing dependency on python3-lz4.
* Set upstream metadata fields: Security-Contact.
* Add upstream signing keys.
* New upstream release.
-- Jelmer Vernooij <jelmer@debian.org> Sat, 27 Mar 2021 17:49:29 +0000
ognibuild (0.0.2-1) unstable; urgency=medium
* New upstream release.
-- Jelmer Vernooij <jelmer@debian.org> Tue, 02 Mar 2021 18:25:45 +0000
ognibuild (0.0.1~git20210228.bc79314-1) unstable; urgency=medium
* New upstream snapshot.
* Add dependency on buildlog-consultant.
-- Jelmer Vernooij <jelmer@debian.org> Sun, 28 Feb 2021 14:55:03 +0000
ognibuild (0.0.1~git20201031.4cbc8df-1) unstable; urgency=low
* Initial release. Closes: #981913
-- Jelmer Vernooij <jelmer@debian.org> Fri, 05 Feb 2021 03:00:40 +0000

43
debian/control vendored
View file

@ -1,43 +0,0 @@
Rules-Requires-Root: no
Standards-Version: 4.6.1
Build-Depends: debhelper-compat (= 13),
dh-sequence-python3,
python3-all,
python3-apt,
python3-breezy,
python3-breezy.tests,
python3-buildlog-consultant (>= 0.0.4),
python3-requirement-parser,
python3-debmutate,
python3-lz4,
python3-setuptools,
brz-debian,
python3-dulwich (>= 0.19.12)
Source: ognibuild
Priority: optional
Section: devel
Maintainer: Jelmer Vernooij <jelmer@debian.org>
Vcs-Git: https://salsa.debian.org/jelmer/ognibuild.git
Vcs-Browser: https://salsa.debian.org/jelmer/ognibuild
Homepage: https://github.com/jelmer/ognibuild
Package: ognibuild
Architecture: all
Depends: python3-apt,
python3-breezy,
python3-buildlog-consultant (>= 0.0.4),
python3-lz4,
python3-requirement-parser,
python3-toml,
${misc:Depends},
${python3:Depends},
Recommends: brz-debian, python3-debmutate
Description: Detect and run any build system
Ognibuild is a simple wrapper with a common interface for invoking any kind of
build tool.
.
The ideas is that it can be run to build and install any source code directory
by detecting the build system that is in use and invoking that with the correct
parameters.
.
It can also detect and install missing dependencies.

28
debian/copyright vendored
View file

@ -1,28 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: ognibuild
Files: *
Copyright: Jelmer Vernooij <jelmer@jelmer.uk>
License: GPL-2+
Files: debian/*
Copyright: Jelmer Vernooij <jelmer@debian.org>
License: GPL-2+
License: GPL-2+
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 2 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, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
.
On Debian systems, the full text of the GNU General Public License is available
in /usr/share/common-licenses/GPL-2.

4
debian/rules vendored
View file

@ -1,4 +0,0 @@
#!/usr/bin/make -f
%:
dh $@ --buildsystem=pybuild

View file

@ -1 +0,0 @@
3.0 (quilt)

View file

@ -1,5 +0,0 @@
Test-Command: python3 -m unittest ognibuild.tests.test_suite
Depends: python3,
@,
lintian-brush, brz-debian, python3-breezy.tests
Restrictions: allow-stderr

View file

@ -1,6 +0,0 @@
---
Bug-Database: https://github.com/jelmer/ognibuild/issues
Bug-Submit: https://github.com/jelmer/ognibuild/issues/new
Repository: https://github.com/jelmer/ognibuild.git
Repository-Browse: https://github.com/jelmer/ognibuild
Security-Contact: https://github.com/jelmer/ognibuild/tree/HEAD/SECURITY.md

View file

@ -1,858 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
opbe/Bbo8mHXG9cKATCsqOGI0/BY9W036WEwadcy24RoZ/DhiJh/+c8Py9u3ZYdf
g56BO3Wy4MdtvSkDuZx0lSRql+X72DD5SveRwQed68CIOkHFWIa5iQezk+XdOqjb
i56T14jUEVWWq6p0d2Jw3obBy/uaEA53oxwAHckOURXMTzXKTqrhqRrQbcOQJTed
RW/8SdL41FNd3NvswjkP9fGnzsPJbOmHHyaZxEgfbZI1NiuQ9hvNwlPHQS3Qzvmu
t58eLwU9ofTDDmG/X8NSmSSKV7nRbENyFQHCKTjmJoqxpj55MKg9JCqlCJfaC1vt
7TkRVVytJ0HahFXOyAiTTLQjSmVsbWVyIFZlcm5vb2lqIDxqZWxtZXJAYXBhY2hl
Lm9yZz6JAlQEEwEKAD4CGwMCHgECF4AFCwkIBwMFFQoJCAsFFgIDAQAWIQTcg37h
Sn43NH6HBhcAgG8r1ymkVwUCYOuSgQUJGWCzAAAKCRAAgG8r1ymkV/nLD/9QbEYT
f0rlHJjgVa1u5FLjyBk9n19jleS11GsnKxmSmdvL3H28mE/MmSPPCHXatRL+ufjk
r7CPD2NJIvClehrr1aS1ZscWsx+DYWRC2JoAPVIgNywn2QdrWh3S3UYsY+9yFkS1
plSskWnaHWz+jJmnO0oI2EeLyTByUL8uY6GYyR6VlfMUi1yj1LH1CGKzr8g8gNNn
tEU4yJxK2svRuL27xNUxGyro8VKzXLnnwEYZQ7lDVd+DxXDPyUwsuAEfOZvQ9OcL
jKGT19sXeGYzCmhM+Rts+rcZaotzKkPxXjq+GWCf2i0DYCUXlpwCx9F8T86GfDlZ
rNJrvWd2XfEITuVt4sxV2zFoQ9gu+akoCY2h6S7CcqJFJ4bvmPJQDjvDazS3Ow+R
wQpk5iK04Asq9qkB+rzq8oeEx0Jra70QKaEqvgcgtJJQHfGcATJJXXC1SvUQYibG
hM3ZxMkL3vhrlRixdsp0oDdVWJMXAKKBskfxL+zerT9Ween0VDguQyS9uKoZ+ba7
hbjUeGFxB+qsuFRZj7U49sjMVRKQnGZW9G2yjqsWdtGLkWC+bVoQGrsZcUG1THin
4xswNL7wtSq2cuLKskPYsTsXGOXODkv1UvXS9ia6K0Im4x/2AQWr3fSxRj93N9y7
k/wbU/MRMFpo5O1AZi0vXOi5TtL4CgJLP3uUMrQjSmVsbWVyIFZlcm5vb2lqIDxq
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
uy6YbxCXn9fsxQQsf9aJ6PB7SKfT1gRbqLzqipfnmQOBQEHUcGC3YugjsWmRm21j
r6ez4l3YPTLVoYL9R8A0Pa9UgIbOcIUDVPacj7I/hj5nDah8ACAl+/baMo3HxfOP
yTV3tYCzfTl+rQKvwlQ0yiFjPSmC5OtyBhW98h0GYxq62niFM050w3MaTmdfxrou
D690eKq8O+naRCH+JTewquYr4dc3N1zGxgagJ57C1sX5ShkP90D3jNFK+U7crJkI
FPfg5K4KbYryAWl/JbXyxoiuZMczV59bUlUugBJ+AhSPp0KAPcKI8BYoHb0Na3Db
Maj41CyEVYCMUl7XWig+DUNXZ1eeA/9WbEXXAoMFeY05xnCV6WVZudcLa1T4qEWU
XSZ2mWdTKXH5POPPV0h3I57oANDCF+E3jq3++RcvSCTTq6SkrAWp+UaL910pySnt
GSlkkN95DN6PhdVgnFArpi5csFrjNJ0cBGdJEKBGJ9/oO9C4x++0JUplbG1lciBW
ZXJub29paiA8amVsbWVyQG5sLmxpbnV4Lm9yZz6JAjwEMAECACYFAk1vtFIfHSBu
bC5saW51eC5vcmcgbm8gbG9uZ2VyIGV4aXN0cwAKCRAAgG8r1ymkVw47D/9VGUUe
AZ59IiTGDfzXCjy3fyDs1dsvAa2PG8mOevCfFBJWd6wbGlkeDKj9OrQvL489i0/M
9RfattXD3QnHqAPZiXsjqudig26s2EmoawZEjIJDXVR0qYO7mOA8S6sSPJzFkrVQ
6Sqd+tV4K5sOKUU2BO685BoLWvDnUzTSyq//wq+SFYThkaS+A54Yot7k1vE24FOI
87JEhVYkParjYsJprJitwz/VwhmTnl0x4+JIIeQMuDQZgx2RuGyiETxgGvc9abmv
osYjXmKGbjdhrAziRV2BJu8MHmSfXWM2454wupVPxUJ+E3xX0n+g993yfgRlyG/C
K6kYKLyJHm7BYiDhY5Avx1P+64I8zR9l5lhTmjFy+OZPjwVJPNkc59eDweHOGU1q
ni1BL5y8eV5SYO8iZej8Of0vbS/k6DAG79aryVEQFVmO4RTGnhr4b2/pIyG1iQV6
1acC8K6LcM5AqWaIvoYqlQ7HdltcF725KyMyxqGGEePJzgxGwWPgFzEJzcgwr+nx
Fa+uuXeX1smVCtGzFZtP3l9U+dQiobs77xO3GX6gi65ucoGfednyVYFuBZszs95U
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
vA==
=e6vU
-----END PGP PUBLIC KEY BLOCK-----

2
debian/watch vendored
View file

@ -1,2 +0,0 @@
version=4
opts=pgpsigurlmangle=s/$/.asc/ https://pypi.debian.net/ognibuild/ognibuild-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))

View file

@ -1,8 +0,0 @@
# See https://github.com/jelmer/disperse
timeout_days: 5
tag_name: "v$VERSION"
verify_command: "python3 -m unittest tests.test_suite"
update_version {
path: "ognibuild/__init__.py"
new_line: "__version__ = $TUPLED_VERSION"
}

View file

@ -0,0 +1,17 @@
Metadata-Version: 2.1
Name: ognibuild
Version: 0.0.7
Summary: Detect and run any build system
Home-page: https://jelmer.uk/code/ognibuild
Maintainer: Jelmer Vernooij
Maintainer-email: jelmer@jelmer.uk
License: GNU GPLv2 or later
Description: UNKNOWN
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Operating System :: POSIX
Provides-Extra: debian

View file

@ -0,0 +1,52 @@
.flake8
.gitignore
AUTHORS
CODE_OF_CONDUCT.md
LICENSE
README.md
SECURITY.md
TODO
releaser.conf
setup.cfg
setup.py
.github/workflows/pythonpackage.yml
notes/architecture.md
notes/concepts.md
notes/roadmap.md
ognibuild/__init__.py
ognibuild/__main__.py
ognibuild/build.py
ognibuild/buildlog.py
ognibuild/buildsystem.py
ognibuild/clean.py
ognibuild/dist.py
ognibuild/dist_catcher.py
ognibuild/fix_build.py
ognibuild/fixers.py
ognibuild/info.py
ognibuild/install.py
ognibuild/outputs.py
ognibuild/requirements.py
ognibuild/test.py
ognibuild/vcs.py
ognibuild.egg-info/PKG-INFO
ognibuild.egg-info/SOURCES.txt
ognibuild.egg-info/dependency_links.txt
ognibuild.egg-info/entry_points.txt
ognibuild.egg-info/requires.txt
ognibuild.egg-info/top_level.txt
ognibuild/debian/__init__.py
ognibuild/debian/apt.py
ognibuild/debian/build.py
ognibuild/debian/build_deps.py
ognibuild/debian/file_search.py
ognibuild/debian/fix_build.py
ognibuild/debian/udd.py
ognibuild/resolver/__init__.py
ognibuild/resolver/apt.py
ognibuild/session/__init__.py
ognibuild/session/plain.py
ognibuild/session/schroot.py
ognibuild/tests/__init__.py
ognibuild/tests/test_debian_build.py
ognibuild/tests/test_debian_fix_build.py

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,4 @@
[console_scripts]
deb-fix-build = ognibuild.debian.fix_build:main
ogni = ognibuild.__main__:main

View file

@ -0,0 +1,8 @@
breezy
buildlog-consultant>=0.0.10
requirements-parser
[debian]
debmutate
python_apt
python_debian

View file

@ -0,0 +1 @@
ognibuild

View file

@ -18,14 +18,12 @@
import os
import stat
from typing import List, Dict, Type
__version__ = (0, 0, 15)
version_string = '.'.join(map(str, __version__))
__version__ = (0, 0, 7)
USER_AGENT = f"Ognibuild/{version_string}"
USER_AGENT = "Ognibuild"
class DetailedFailure(Exception):
@ -34,12 +32,6 @@ class DetailedFailure(Exception):
self.argv = argv
self.error = error
def __eq__(self, other):
return (isinstance(other, type(self)) and
self.retcode == other.retcode and
self.argv == other.argv and
self.error == other.error)
class UnidentifiedError(Exception):
"""An unidentified error."""
@ -50,13 +42,6 @@ class UnidentifiedError(Exception):
self.lines = lines
self.secondary = secondary
def __eq__(self, other):
return (isinstance(other, type(self)) and
self.retcode == other.retcode and
self.argv == other.argv and
self.lines == other.lines and
self.secondary == other.secondary)
def __repr__(self):
return "<%s(%r, %r, ..., secondary=%r)>" % (
type(self).__name__,
@ -79,64 +64,17 @@ def shebang_binary(p):
return os.path.basename(args[0].decode()).strip()
class UnknownRequirementFamily(Exception):
"""Requirement family is unknown"""
def __init__(self, family):
self.family = family
class Requirement(object):
# Name of the family of requirements - e.g. "python-package"
family: str
_JSON_DESERIALIZERS: Dict[str, Type["Requirement"]] = {}
@classmethod
def _from_json(self, js):
raise NotImplementedError(self._from_json)
@classmethod
def from_json(self, js):
try:
family = Requirement._JSON_DESERIALIZERS[js[0]]
except KeyError:
raise UnknownRequirementFamily(js[0])
return family._from_json(js[1])
def __init__(self, family):
self.family = family
def met(self, session):
raise NotImplementedError(self)
def _json(self):
raise NotImplementedError(self._json)
def json(self):
return (type(self).family, self._json())
@classmethod
def register_json(cls, subcls):
Requirement._JSON_DESERIALIZERS[subcls.family] = subcls
class OneOfRequirement(Requirement):
elements: List[Requirement]
family = 'or'
def __init__(self, elements):
self.elements = elements
def met(self, session):
for req in self.elements:
if req.met(session):
return True
return False
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.elements)
class UpstreamOutput(object):
def __init__(self, family):

View file

@ -15,13 +15,11 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from contextlib import ExitStack
import logging
import os
import shlex
import sys
from urllib.parse import urlparse
from . import UnidentifiedError, DetailedFailure, version_string
from . import UnidentifiedError, DetailedFailure
from .buildlog import (
InstallFixer,
ExplainInstallFixer,
@ -31,10 +29,9 @@ from .buildlog import (
from .buildsystem import NoBuildToolsFound, detect_buildsystems
from .resolver import (
auto_resolver,
select_resolvers,
UnsatisfiedRequirements,
native_resolvers,
)
from .session import SessionSetupFailure
from .resolver.apt import AptResolver
def display_explain_commands(commands):
@ -42,33 +39,34 @@ def display_explain_commands(commands):
for command, reqs in commands:
if isinstance(command, list):
command = shlex.join(command)
logging.info(
" %s (to install %s)", command, ", ".join(map(str, reqs)))
logging.info(" %s (to install %s)", command, ", ".join(map(str, reqs)))
def get_necessary_declared_requirements(resolver, requirements, stages):
missing = []
for stage, req in requirements:
if stage in stages:
missing.append(req)
return missing
def install_necessary_declared_requirements(
session, resolver, fixers, buildsystems, stages, explain=False
):
relevant = []
declared_reqs = []
for buildsystem in buildsystems:
try:
declared_reqs.extend(buildsystem.get_declared_dependencies(session, fixers))
except NotImplementedError:
logging.warning(
"Unable to determine declared dependencies from %r", buildsystem
)
relevant.extend(
get_necessary_declared_requirements(resolver, declared_reqs, stages)
)
if explain:
relevant = []
for buildsystem in buildsystems:
declared_reqs = buildsystem.get_declared_dependencies(
session, fixers)
for stage, req in declared_reqs:
if stage in stages:
relevant.append(req)
install_missing_reqs(session, resolver, relevant, explain=True)
else:
for buildsystem in buildsystems:
try:
buildsystem.install_declared_requirements(
stages, session, resolver, fixers)
except NotImplementedError:
logging.warning(
"Unable to determine declared dependencies from %r",
buildsystem
)
install_missing_reqs(session, resolver, relevant, explain=explain)
# Types of dependencies:
@ -84,7 +82,6 @@ STAGE_MAP = {
"test": ["test", "build", "core"],
"build": ["build", "core"],
"clean": [],
"verify": ["build", "core", "test"],
}
@ -98,13 +95,9 @@ def determine_fixers(session, resolver, explain=False):
def main(): # noqa: C901
import argparse
parser = argparse.ArgumentParser(prog='ogni')
parser = argparse.ArgumentParser()
parser.add_argument(
"--version", action="version", version="%(prog)s " + version_string
)
parser.add_argument(
"--directory", "-d", type=str, help="Directory for project.",
default="."
"--directory", "-d", type=str, help="Directory for project.", default="."
)
parser.add_argument("--schroot", type=str, help="schroot to run in.")
parser.add_argument(
@ -130,15 +123,6 @@ def main(): # noqa: C901
action="store_true",
help="Ignore declared dependencies, follow build errors only",
)
parser.add_argument(
"--user", action="store_true",
help="Install in local-user directories."
)
parser.add_argument(
"--dep-server-url", type=str,
help="ognibuild dep server to use",
default=os.environ.get('OGNIBUILD_DEPS'))
parser.add_argument("--verbose", action="store_true", help="Be verbose")
subparsers = parser.add_subparsers(dest="subcommand")
subparsers.add_parser("dist")
@ -146,11 +130,12 @@ def main(): # noqa: C901
subparsers.add_parser("clean")
subparsers.add_parser("test")
subparsers.add_parser("info")
subparsers.add_parser("verify")
exec_parser = subparsers.add_parser("exec")
exec_parser.add_argument(
'subargv', nargs=argparse.REMAINDER, help='Command to run.')
exec_parser.add_argument('subargv', nargs=argparse.REMAINDER, help='Command to run.')
install_parser = subparsers.add_parser("install")
install_parser.add_argument(
"--user", action="store_true", help="Install in local-user directories."
)
install_parser.add_argument(
"--prefix", type=str, help='Prefix to install in')
@ -170,72 +155,38 @@ def main(): # noqa: C901
from .session.plain import PlainSession
session = PlainSession()
with ExitStack() as es:
try:
es.enter_context(session)
except SessionSetupFailure as e:
logging.debug('Error lines: %r', e.errlines)
logging.fatal('Failed to set up session: %s', e.reason)
return 1
parsed_url = urlparse(args.directory)
# TODO(jelmer): Get a list of supported schemes from breezy?
if parsed_url.scheme in ('git', 'http', 'https', 'ssh'):
import breezy.git # noqa: F401
import breezy.bzr # noqa: F401
from breezy.branch import Branch
from silver_platter.utils import TemporarySprout
b = Branch.open(args.directory)
logging.info("Cloning %s", args.directory)
wt = es.enter_context(TemporarySprout(b))
external_dir, internal_dir = session.setup_from_vcs(wt)
else:
if parsed_url.scheme == 'file':
directory = parsed_url.path
else:
directory = args.directory
logging.info("Preparing directory %s", directory)
external_dir, internal_dir = session.setup_from_directory(
directory)
with session:
logging.info("Preparing directory %s", args.directory)
external_dir, internal_dir = session.setup_from_directory(args.directory)
session.chdir(internal_dir)
os.chdir(external_dir)
if not session.is_temporary and args.subcommand == 'info':
args.explain = True
if args.resolve == "auto":
if args.resolve == "apt":
resolver = AptResolver.from_session(session)
elif args.resolve == "native":
resolver = native_resolvers(session, user_local=args.user)
elif args.resolve == "auto":
resolver = auto_resolver(session, explain=args.explain)
else:
resolver = select_resolvers(
session, user_local=args.user,
resolvers=args.resolve.split(','),
dep_server_url=args.dep_server_url)
logging.info("Using requirement resolver: %s", resolver)
fixers = determine_fixers(session, resolver, explain=args.explain)
try:
if args.subcommand == "exec":
from .fix_build import run_with_build_fixers
run_with_build_fixers(fixers, session, args.subargv)
run_with_build_fixers(session, args.subargv, fixers)
return 0
bss = list(detect_buildsystems(external_dir))
bss = list(detect_buildsystems(args.directory))
logging.info("Detected buildsystems: %s", ", ".join(map(str, bss)))
if not args.ignore_declared_dependencies:
stages = STAGE_MAP[args.subcommand]
if stages:
logging.info(
"Checking that declared requirements are present")
logging.info("Checking that declared requirements are present")
try:
install_necessary_declared_requirements(
session, resolver, fixers, bss, stages,
explain=args.explain
session, resolver, fixers, bss, stages, explain=args.explain
)
except UnsatisfiedRequirements as e:
logging.info(
'Unable to install declared dependencies:')
for req in e.requirements:
logging.info(' * %s', req)
return 1
except ExplainInstall as e:
display_explain_commands(e.commands)
return 1
@ -256,15 +207,11 @@ def main(): # noqa: C901
if args.subcommand == "build":
from .build import run_build
run_build(
session, buildsystems=bss, resolver=resolver,
fixers=fixers)
run_build(session, buildsystems=bss, resolver=resolver, fixers=fixers)
if args.subcommand == "clean":
from .clean import run_clean
run_clean(
session, buildsystems=bss, resolver=resolver,
fixers=fixers)
run_clean(session, buildsystems=bss, resolver=resolver, fixers=fixers)
if args.subcommand == "install":
from .install import run_install
@ -279,42 +226,14 @@ def main(): # noqa: C901
if args.subcommand == "test":
from .test import run_test
run_test(
session, buildsystems=bss, resolver=resolver,
fixers=fixers)
run_test(session, buildsystems=bss, resolver=resolver, fixers=fixers)
if args.subcommand == "info":
from .info import run_info
run_info(session, buildsystems=bss, fixers=fixers)
if args.subcommand == "verify":
from .build import run_build
from .test import run_test
run_build(
session, buildsystems=bss, resolver=resolver,
fixers=fixers)
run_test(
session, buildsystems=bss, resolver=resolver,
fixers=fixers)
except ExplainInstall as e:
display_explain_commands(e.commands)
except UnidentifiedError:
logging.info(
'If there is a clear indication of a problem in the build '
'log, please consider filing a request to update the patterns '
'in buildlog-consultant at '
'https://github.com/jelmer/buildlog-consultant/issues/new')
return 1
except DetailedFailure:
if not args.verbose:
logging.info(
'Run with --verbose to get more information '
'about steps taken to try to resolve error')
logging.info(
'Please consider filing a bug report at '
'https://github.com/jelmer/ognibuild/issues/new')
except (UnidentifiedError, DetailedFailure):
return 1
except NoBuildToolsFound:
logging.info("No build tools found.")

View file

@ -15,28 +15,16 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from functools import partial
from .buildsystem import NoBuildToolsFound
from .fix_build import iterate_with_build_fixers
from .logs import NoLogManager
BUILD_LOG_FILENAME = 'build.log'
def run_build(session, buildsystems, resolver, fixers, log_manager=None):
def run_build(session, buildsystems, resolver, fixers):
# Some things want to write to the user's home directory,
# e.g. pip caches in ~/.cache
session.create_home()
if log_manager is None:
log_manager = NoLogManager()
for buildsystem in buildsystems:
iterate_with_build_fixers(
fixers, log_manager.wrap(
partial(buildsystem.build, session, resolver)))
buildsystem.build(session, resolver, fixers)
return
raise NoBuildToolsFound()

View file

@ -19,32 +19,65 @@
"""
import logging
from typing import Optional, List, Callable, Union, Tuple
from buildlog_consultant.common import (
Problem,
MissingPythonModule,
MissingPythonDistribution,
MissingCHeader,
MissingPkgConfig,
MissingCommand,
MissingFile,
MissingJavaScriptRuntime,
MissingSprocketsFile,
MissingGoPackage,
MissingPerlFile,
MissingPerlModule,
MissingXmlEntity,
MissingJDKFile,
MissingJDK,
MissingJRE,
MissingNodeModule,
MissingNodePackage,
MissingPhpClass,
MissingRubyGem,
MissingLibrary,
MissingSetupPyCommand,
MissingCMakeComponents,
MissingJavaClass,
MissingCSharpCompiler,
MissingRPackage,
MissingRubyFile,
MissingAutoconfMacro,
MissingValaPackage,
MissingBoostComponents,
MissingXfceDependency,
MissingHaskellDependencies,
MissingVagueDependency,
DhAddonLoadFailure,
MissingMavenArtifacts,
MissingIntrospectionTypelib,
GnomeCommonMissing,
MissingGnomeCommonDependency,
UnknownCertificateAuthority,
CMakeFilesMissing,
MissingLibtool,
MissingQt,
MissingX11,
MissingPerlPredeclared,
MissingLatexFile,
MissingCargoCrate,
MissingStaticLibrary,
)
from buildlog_consultant.apt import UnsatisfiedAptDependencies
from . import OneOfRequirement
from .fix_build import BuildFixer
from .requirements import (
Requirement,
BinaryRequirement,
PathRequirement,
PkgConfigRequirement,
CHeaderRequirement,
JavaScriptRuntimeRequirement,
ValaPackageRequirement,
RubyGemRequirement,
GoPackageRequirement,
DhAddonRequirement,
PhpClassRequirement,
@ -59,7 +92,6 @@ from .requirements import (
HaskellPackageRequirement,
MavenArtifactRequirement,
BoostComponentRequirement,
KF5ComponentRequirement,
GnomeCommonRequirement,
JDKFileRequirement,
JDKRequirement,
@ -80,124 +112,86 @@ from .requirements import (
LatexPackageRequirement,
CargoCrateRequirement,
StaticLibraryRequirement,
GnulibDirectoryRequirement,
LuaModuleRequirement,
PHPExtensionRequirement,
VcsControlDirectoryAccessRequirement,
RubyGemRequirement,
QtModuleRequirement,
)
from .resolver import UnsatisfiedRequirements
def map_pytest_arguments_to_plugin(args):
# TODO(jelmer): Map argument to PytestPluginRequirement
return None
ProblemToRequirementConverter = Callable[[Problem], Optional[Requirement]]
PROBLEM_CONVERTERS: List[Union[
Tuple[str, ProblemToRequirementConverter],
Tuple[str, ProblemToRequirementConverter, str]]] = [
('missing-file', lambda p: PathRequirement(p.path)),
('command-missing', lambda p: BinaryRequirement(p.command)),
('valac-cannot-compile', lambda p: VagueDependencyRequirement('valac'),
'0.0.27'),
('missing-cmake-files', lambda p: OneOfRequirement(
[CMakefileRequirement(filename, p.version)
for filename in p.filenames])),
('missing-command-or-build-file', lambda p: BinaryRequirement(p.command)),
('missing-pkg-config-package',
lambda p: PkgConfigRequirement(p.module, p.minimum_version)),
('missing-c-header', lambda p: CHeaderRequirement(p.header)),
('missing-introspection-typelib',
lambda p: IntrospectionTypelibRequirement(p.library)),
('missing-python-module', lambda p: PythonModuleRequirement(
p.module, python_version=p.python_version,
minimum_version=p.minimum_version)),
('missing-python-distribution', lambda p: PythonPackageRequirement(
p.distribution, python_version=p.python_version,
minimum_version=p.minimum_version)),
('javascript-runtime-missing', lambda p: JavaScriptRuntimeRequirement()),
('missing-node-module', lambda p: NodeModuleRequirement(p.module)),
('missing-node-package', lambda p: NodePackageRequirement(p.package)),
('missing-ruby-gem', lambda p: RubyGemRequirement(p.gem, p.version)),
('missing-qt-modules', lambda p: QtModuleRequirement(p.modules[0]),
'0.0.27'),
('missing-php-class', lambda p: PhpClassRequirement(p.php_class)),
('missing-r-package', lambda p: RPackageRequirement(
p.package, p.minimum_version)),
('missing-vague-dependency',
lambda p: VagueDependencyRequirement(
p.name, minimum_version=p.minimum_version)),
('missing-c#-compiler', lambda p: BinaryRequirement("msc")),
('missing-gnome-common', lambda p: GnomeCommonRequirement()),
('missing-jdk', lambda p: JDKRequirement()),
('missing-jre', lambda p: JRERequirement()),
('missing-qt', lambda p: QTRequirement()),
('missing-x11', lambda p: X11Requirement()),
('missing-libtool', lambda p: LibtoolRequirement()),
('missing-php-extension',
lambda p: PHPExtensionRequirement(p.extension)),
('missing-rust-compiler', lambda p: BinaryRequirement("rustc")),
('missing-java-class', lambda p: JavaClassRequirement(p.classname)),
('missing-go-package', lambda p: GoPackageRequirement(p.package)),
('missing-autoconf-macro', lambda p: AutoconfMacroRequirement(p.macro)),
('missing-vala-package', lambda p: ValaPackageRequirement(p.package)),
('missing-lua-module', lambda p: LuaModuleRequirement(p.module)),
('missing-jdk-file', lambda p: JDKFileRequirement(p.jdk_path, p.filename)),
('missing-ruby-file', lambda p: RubyFileRequirement(p.filename)),
('missing-library', lambda p: LibraryRequirement(p.library)),
('missing-sprockets-file',
lambda p: SprocketsFileRequirement(p.content_type, p.name)),
('dh-addon-load-failure', lambda p: DhAddonRequirement(p.path)),
('missing-xml-entity', lambda p: XmlEntityRequirement(p.url)),
('missing-gnulib-directory',
lambda p: GnulibDirectoryRequirement(p.directory)),
('vcs-control-directory-needed',
lambda p: VcsControlDirectoryAccessRequirement(p.vcs)),
('missing-static-library',
lambda p: StaticLibraryRequirement(p.library, p.filename)),
('missing-perl-module',
lambda p: PerlModuleRequirement(
module=p.module, filename=p.filename, inc=p.inc)),
('unknown-certificate-authority',
lambda p: CertificateAuthorityRequirement(p.url)),
('unsupported-pytest-arguments',
lambda p: map_pytest_arguments_to_plugin(p.args), '0.0.27'),
]
def problem_to_upstream_requirement(
problem: Problem) -> Optional[Requirement]: # noqa: C901
for entry in PROBLEM_CONVERTERS:
kind, fn = entry[:2]
if kind == problem.kind:
return fn(problem)
if isinstance(problem, MissingCMakeComponents):
if problem.name.lower() == 'boost':
return OneOfRequirement(
[BoostComponentRequirement(name)
for name in problem.components])
elif problem.name.lower() == 'kf5':
return OneOfRequirement(
[KF5ComponentRequirement(name) for name in problem.components])
return None
def problem_to_upstream_requirement(problem): # noqa: C901
if isinstance(problem, MissingFile):
return PathRequirement(problem.path)
elif isinstance(problem, MissingCommand):
return BinaryRequirement(problem.command)
elif isinstance(problem, MissingPkgConfig):
return PkgConfigRequirement(problem.module, problem.minimum_version)
elif isinstance(problem, MissingCHeader):
return CHeaderRequirement(problem.header)
elif isinstance(problem, MissingIntrospectionTypelib):
return IntrospectionTypelibRequirement(problem.library)
elif isinstance(problem, MissingJavaScriptRuntime):
return JavaScriptRuntimeRequirement()
elif isinstance(problem, MissingRubyGem):
return RubyGemRequirement(problem.gem, problem.version)
elif isinstance(problem, MissingValaPackage):
return ValaPackageRequirement(problem.package)
elif isinstance(problem, MissingGoPackage):
return GoPackageRequirement(problem.package)
elif isinstance(problem, MissingBoostComponents):
return [BoostComponentRequirement(name) for name in problem.components]
elif isinstance(problem, DhAddonLoadFailure):
return DhAddonRequirement(problem.path)
elif isinstance(problem, MissingPhpClass):
return PhpClassRequirement(problem.php_class)
elif isinstance(problem, MissingRPackage):
return RPackageRequirement(problem.package, problem.minimum_version)
elif isinstance(problem, MissingNodeModule):
return NodeModuleRequirement(problem.module)
elif isinstance(problem, MissingStaticLibrary):
return StaticLibraryRequirement(problem.library, problem.filename)
elif isinstance(problem, MissingNodePackage):
return NodePackageRequirement(problem.package)
elif isinstance(problem, MissingLatexFile):
if problem.filename.endswith('.sty'):
return LatexPackageRequirement(problem.filename[:-4])
return None
elif isinstance(problem, MissingVagueDependency):
return VagueDependencyRequirement(problem.name, minimum_version=problem.minimum_version)
elif isinstance(problem, MissingLibrary):
return LibraryRequirement(problem.library)
elif isinstance(problem, MissingRubyFile):
return RubyFileRequirement(problem.filename)
elif isinstance(problem, MissingXmlEntity):
return XmlEntityRequirement(problem.url)
elif isinstance(problem, MissingSprocketsFile):
return SprocketsFileRequirement(problem.content_type, problem.name)
elif isinstance(problem, MissingJavaClass):
return JavaClassRequirement(problem.classname)
elif isinstance(problem, CMakeFilesMissing):
return [CMakefileRequirement(filename) for filename in problem.filenames]
elif isinstance(problem, MissingHaskellDependencies):
return OneOfRequirement(
[HaskellPackageRequirement.from_string(dep)
for dep in problem.deps])
return [HaskellPackageRequirement.from_string(dep) for dep in problem.deps]
elif isinstance(problem, MissingMavenArtifacts):
return OneOfRequirement([
return [
MavenArtifactRequirement.from_str(artifact)
for artifact in problem.artifacts
])
]
elif isinstance(problem, MissingCSharpCompiler):
return BinaryRequirement("msc")
elif isinstance(problem, GnomeCommonMissing):
return GnomeCommonRequirement()
elif isinstance(problem, MissingJDKFile):
return JDKFileRequirement(problem.jdk_path, problem.filename)
elif isinstance(problem, MissingJDK):
return JDKRequirement()
elif isinstance(problem, MissingJRE):
return JRERequirement()
elif isinstance(problem, MissingQt):
return QTRequirement()
elif isinstance(problem, MissingX11):
return X11Requirement()
elif isinstance(problem, MissingLibtool):
return LibtoolRequirement()
elif isinstance(problem, UnknownCertificateAuthority):
return CertificateAuthorityRequirement(problem.url)
elif isinstance(problem, MissingPerlPredeclared):
ret = PerlPreDeclaredRequirement(problem.name)
try:
@ -216,20 +210,36 @@ def problem_to_upstream_requirement(
return BinaryRequirement("glib-gettextize")
else:
logging.warning(
"No known command for gnome-common dependency %s",
problem.package
"No known command for gnome-common dependency %s", problem.package
)
return None
elif isinstance(problem, MissingXfceDependency):
if problem.package == "gtk-doc":
return BinaryRequirement("gtkdocize")
else:
logging.warning(
"No known command for xfce dependency %s", problem.package)
logging.warning("No known command for xfce dependency %s", problem.package)
return None
elif isinstance(problem, MissingPerlModule):
return PerlModuleRequirement(
module=problem.module, filename=problem.filename, inc=problem.inc
)
elif isinstance(problem, MissingPerlFile):
return PerlFileRequirement(filename=problem.filename)
elif problem.kind == 'unsatisfied-apt-dependencies':
elif isinstance(problem, MissingAutoconfMacro):
return AutoconfMacroRequirement(problem.macro)
elif isinstance(problem, MissingPythonModule):
return PythonModuleRequirement(
problem.module,
python_version=problem.python_version,
minimum_version=problem.minimum_version,
)
elif isinstance(problem, MissingPythonDistribution):
return PythonPackageRequirement(
problem.distribution,
python_version=problem.python_version,
minimum_version=problem.minimum_version,
)
elif isinstance(problem, UnsatisfiedAptDependencies):
from .resolver.apt import AptRequirement
return AptRequirement(problem.relations)
else:

File diff suppressed because it is too large Load diff

View file

@ -15,25 +15,16 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from functools import partial
from .fix_build import iterate_with_build_fixers
from .buildsystem import NoBuildToolsFound
from .logs import NoLogManager
def run_clean(session, buildsystems, resolver, fixers, log_manager=None):
def run_clean(session, buildsystems, resolver, fixers):
# Some things want to write to the user's home directory,
# e.g. pip caches in ~/.cache
session.create_home()
if log_manager is None:
log_manager = NoLogManager()
for buildsystem in buildsystems:
iterate_with_build_fixers(
fixers, log_manager.wrap(
partial(buildsystem.clean, session, resolver)))
buildsystem.clean(session, resolver, fixers)
return
raise NoBuildToolsFound()

View file

@ -29,8 +29,7 @@ def satisfy_build_deps(session: Session, tree, debian_path):
deps.append(source[name].strip().strip(","))
except KeyError:
pass
for name in ["Build-Conflicts", "Build-Conflicts-Indep",
"Build-Conflicts-Arch"]:
for name in ["Build-Conflicts", "Build-Conflicts-Indep", "Build-Conflicts-Arch"]:
try:
deps.append("Conflicts: " + source[name])
except KeyError:

View file

@ -16,9 +16,8 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from debian.changelog import Version
import logging
from typing import List, Optional, Iterable
from typing import List, Optional
import os
from buildlog_consultant.apt import (
@ -38,12 +37,7 @@ from .file_search import (
def run_apt(
session: Session, args: List[str], prefix: Optional[List[str]] = None
) -> None:
"""Run apt.
Raises:
DetailedFailure: When a known error occurs
UnidentifiedError: If an unknown error occurs
"""
"""Run apt."""
if prefix is None:
prefix = []
args = prefix = ["apt", "-y"] + args
@ -54,7 +48,7 @@ def run_apt(
match, error = find_apt_get_failure(lines)
if error is not None:
raise DetailedFailure(retcode, args, error)
while lines and lines[-1].rstrip('\n') == "":
while lines and lines[-1] == "":
lines.pop(-1)
raise UnidentifiedError(retcode, args, lines, secondary=match)
@ -99,18 +93,13 @@ class AptManager(object):
def package_exists(self, package):
return package in self.apt_cache
def package_versions(self, package: str) -> Optional[Iterable[Version]]:
try:
return list(self.apt_cache[package].versions)
except KeyError:
return None
def package_versions(self, package):
return list(self.apt_cache[package].versions)
async def get_packages_for_paths(
self, paths, regex: bool = False, case_insensitive: bool = False):
def get_packages_for_paths(self, paths, regex=False, case_insensitive=False):
logging.debug("Searching for packages containing %r", paths)
return await get_packages_for_paths(
paths, self.searchers(), regex=regex,
case_insensitive=case_insensitive
return get_packages_for_paths(
paths, self.searchers(), regex=regex, case_insensitive=case_insensitive
)
def missing(self, packages):

View file

@ -17,7 +17,6 @@
__all__ = [
"get_build_architecture",
"version_add_suffix",
"add_dummy_changelog_entry",
"build",
"DetailedDebianBuildFailure",
@ -25,22 +24,20 @@ __all__ = [
]
from datetime import datetime
from debmutate.changelog import ChangelogEditor
import logging
import os
import re
import shlex
import subprocess
import sys
from typing import Optional, List, Tuple
from debian.changelog import Changelog, Version, ChangeBlock
from debmutate.changelog import get_maintainer, ChangelogEditor
from debmutate.reformatting import GeneratedFile
from debian.changelog import Changelog
from debmutate.changelog import get_maintainer
from breezy.mutabletree import MutableTree
from breezy.plugins.debian.builder import BuildFailedError
from breezy.tree import Tree
from breezy.workingtree import WorkingTree
from buildlog_consultant.sbuild import (
worker_failure_from_sbuild_log,
@ -48,18 +45,10 @@ from buildlog_consultant.sbuild import (
from .. import DetailedFailure as DetailedFailure, UnidentifiedError
BUILD_LOG_FILENAME = 'build.log'
DEFAULT_BUILDER = "sbuild --no-clean-source"
class ChangelogNotEditable(Exception):
"""Changelog can not be edited."""
def __init__(self, path):
self.path = path
class DetailedDebianBuildFailure(DetailedFailure):
def __init__(self, stage, phase, retcode, argv, error, description):
@ -71,8 +60,7 @@ class DetailedDebianBuildFailure(DetailedFailure):
class UnidentifiedDebianBuildError(UnidentifiedError):
def __init__(self, stage, phase, retcode, argv, lines, description,
secondary=None):
def __init__(self, stage, phase, retcode, argv, lines, description, secondary=None):
super(UnidentifiedDebianBuildError, self).__init__(
retcode, argv, lines, secondary)
self.stage = stage
@ -87,12 +75,11 @@ class MissingChangesFile(Exception):
self.filename = filename
def find_changes_files(path: str, package: str, version: Version):
non_epoch_version = version.upstream_version or ''
def find_changes_files(path, package, version):
non_epoch_version = version.upstream_version
if version.debian_version is not None:
non_epoch_version += "-%s" % version.debian_version
c = re.compile('%s_%s_(.*).changes' % (
re.escape(package), re.escape(non_epoch_version)))
c = re.compile('%s_%s_(.*).changes' % (re.escape(package), re.escape(non_epoch_version)))
for entry in os.scandir(path):
m = c.match(entry.name)
if m:
@ -122,32 +109,15 @@ def control_files_in_root(tree: Tree, subpath: str) -> bool:
return False
def version_add_suffix(version: Version, suffix: str) -> Version:
version = Version(str(version))
def add_suffix(v):
m = re.fullmatch("(.*)(" + re.escape(suffix) + ")([0-9]+)", v)
if m:
return m.group(1) + m.group(2) + "%d" % (int(m.group(3)) + 1)
else:
return v + suffix + "1"
if version.debian_revision:
version.debian_revision = add_suffix(version.debian_revision)
else:
version.upstream_version = add_suffix(version.upstream_version)
return version
def add_dummy_changelog_entry(
tree: MutableTree,
subpath: str,
suffix: str,
suite: str,
message: str,
timestamp: Optional[datetime] = None,
maintainer: Tuple[Optional[str], Optional[str]] = None,
allow_reformatting: bool = True,
) -> Version:
timestamp=None,
maintainer=None,
):
"""Add a dummy changelog entry to a package.
Args:
@ -155,10 +125,18 @@ def add_dummy_changelog_entry(
suffix: Suffix for the version
suite: Debian suite
message: Changelog message
Returns:
version of the newly added entry
"""
def add_suffix(v, suffix):
m = re.fullmatch(
"(.*)(" + re.escape(suffix) + ")([0-9]+)",
v,
)
if m:
return m.group(1) + m.group(2) + "%d" % (int(m.group(3)) + 1)
else:
return v + suffix + "1"
if control_files_in_root(tree, subpath):
path = os.path.join(subpath, "changelog")
else:
@ -167,38 +145,38 @@ def add_dummy_changelog_entry(
maintainer = get_maintainer()
if timestamp is None:
timestamp = datetime.now()
try:
with ChangelogEditor(
tree.abspath(path), # type: ignore
allow_reformatting=allow_reformatting) as editor:
version = version_add_suffix(editor[0].version, suffix)
editor.auto_version(version, timestamp=timestamp)
editor.add_entry(
summary=[message], maintainer=maintainer, timestamp=timestamp,
urgency='low')
editor[0].distributions = suite
return version
except GeneratedFile as e:
raise ChangelogNotEditable(path) from e
with ChangelogEditor(tree.abspath(os.path.join(path))) as editor:
version = editor[0].version
if version.debian_revision:
version.debian_revision = add_suffix(version.debian_revision, suffix)
else:
version.upstream_version = add_suffix(version.upstream_version, suffix)
editor.auto_version(version, timestamp=timestamp)
editor.add_entry(
summary=[message], maintainer=maintainer, timestamp=timestamp, urgency='low')
editor[0].distributions = suite
def get_latest_changelog_entry(
local_tree: WorkingTree, subpath: str = "") -> ChangeBlock:
def get_latest_changelog_entry(local_tree, subpath=""):
if control_files_in_root(local_tree, subpath):
path = os.path.join(subpath, "changelog")
else:
path = os.path.join(subpath, "debian", "changelog")
with local_tree.get_file(path) as f:
cl = Changelog(f, max_blocks=1)
return cl[0]
return cl.package, cl.version
def _builddeb_command(
build_command: str = DEFAULT_BUILDER,
result_dir: Optional[str] = None,
apt_repository: Optional[str] = None,
apt_repository_key: Optional[str] = None,
extra_repositories: Optional[List[str]] = None):
def build(
local_tree,
outf,
build_command=DEFAULT_BUILDER,
result_dir=None,
distribution=None,
subpath="",
source_date_epoch=None,
extra_repositories=None,
):
for repo in extra_repositories or []:
build_command += " --extra-repository=" + shlex.quote(repo)
args = [
@ -209,34 +187,8 @@ def _builddeb_command(
"--guess-upstream-branch-url",
"--builder=%s" % build_command,
]
if apt_repository:
args.append("--apt-repository=%s" % apt_repository)
if apt_repository_key:
args.append("--apt-repository-key=%s" % apt_repository_key)
if result_dir:
args.append("--result-dir=%s" % result_dir)
return args
def build(
local_tree: WorkingTree,
outf,
build_command: str = DEFAULT_BUILDER,
result_dir: Optional[str] = None,
distribution: Optional[str] = None,
subpath: str = "",
source_date_epoch: Optional[int] = None,
apt_repository: Optional[str] = None,
apt_repository_key: Optional[str] = None,
extra_repositories: Optional[List[str]] = None,
):
args = _builddeb_command(
build_command=build_command,
result_dir=result_dir,
apt_repository=apt_repository,
apt_repository_key=apt_repository_key,
extra_repositories=extra_repositories)
outf.write("Running %r\n" % (build_command,))
outf.flush()
env = dict(os.environ.items())
@ -247,25 +199,22 @@ def build(
logging.info("Building debian packages, running %r.", build_command)
try:
subprocess.check_call(
args, cwd=local_tree.abspath(subpath), stdout=outf, stderr=outf,
env=env
args, cwd=local_tree.abspath(subpath), stdout=outf, stderr=outf, env=env
)
except subprocess.CalledProcessError:
raise BuildFailedError()
def build_once(
local_tree: WorkingTree,
build_suite: str,
output_directory: str,
build_command: str,
subpath: str = "",
source_date_epoch: Optional[int] = None,
apt_repository: Optional[str] = None,
apt_repository_key: Optional[str] = None,
extra_repositories: Optional[List[str]] = None
local_tree,
build_suite,
output_directory,
build_command,
subpath="",
source_date_epoch=None,
extra_repositories=None
):
build_log_path = os.path.join(output_directory, BUILD_LOG_FILENAME)
build_log_path = os.path.join(output_directory, "build.log")
logging.debug("Writing build log to %s", build_log_path)
try:
with open(build_log_path, "w") as f:
@ -277,8 +226,6 @@ def build_once(
distribution=build_suite,
subpath=subpath,
source_date_epoch=source_date_epoch,
apt_repository=apt_repository,
apt_repository_key=apt_repository_key,
extra_repositories=extra_repositories,
)
except BuildFailedError as e:
@ -300,39 +247,27 @@ def build_once(
[], sbuild_failure.description)
cl_entry = get_latest_changelog_entry(local_tree, subpath)
if cl_entry.package is None:
raise Exception('missing package in changelog entry')
changes_names = []
for kind, entry in find_changes_files(
output_directory, cl_entry.package, cl_entry.version):
for kind, entry in find_changes_files(output_directory, cl_entry.package, cl_entry.version):
changes_names.append((entry.name))
return (changes_names, cl_entry)
class GitBuildpackageMissing(Exception):
"""git-buildpackage is not installed"""
def gbp_dch(path):
try:
subprocess.check_call(["gbp", "dch", "--ignore-branch"], cwd=path)
except FileNotFoundError:
raise GitBuildpackageMissing()
subprocess.check_call(["gbp", "dch", "--ignore-branch"], cwd=path)
def attempt_build(
local_tree: WorkingTree,
suffix: str,
build_suite: str,
output_directory: str,
build_command: str,
build_changelog_entry: Optional[str] = None,
subpath: str = "",
source_date_epoch: Optional[int] = None,
run_gbp_dch: bool = False,
apt_repository: Optional[str] = None,
apt_repository_key: Optional[str] = None,
extra_repositories: Optional[List[str]] = None
local_tree,
suffix,
build_suite,
output_directory,
build_command,
build_changelog_entry=None,
subpath="",
source_date_epoch=None,
run_gbp_dch=False,
extra_repositories=None
):
"""Attempt a build, with a custom distribution set.
@ -347,7 +282,7 @@ def attempt_build(
source_date_epoch: Source date epoch to set
Returns: Tuple with (changes_name, cl_version)
"""
if run_gbp_dch and not subpath and hasattr(local_tree.controldir, '_git'):
if run_gbp_dch and not subpath:
gbp_dch(local_tree.abspath(subpath))
if build_changelog_entry is not None:
add_dummy_changelog_entry(
@ -360,7 +295,5 @@ def attempt_build(
build_command,
subpath,
source_date_epoch=source_date_epoch,
apt_repository=apt_repository,
apt_repository_key=apt_repository_key,
extra_repositories=extra_repositories,
)

View file

@ -18,46 +18,43 @@
"""Tie breaking by build deps."""
from debian.deb822 import PkgRelation
import logging
from breezy.plugins.debian.apt_repo import LocalApt, NoAptSources
class BuildDependencyTieBreaker(object):
def __init__(self, apt):
self.apt = apt
def __init__(self, rootdir):
self.rootdir = rootdir
self._counts = None
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.apt)
return "%s(%r)" % (type(self).__name__, self.rootdir)
@classmethod
def from_session(cls, session):
return cls(LocalApt(session.location))
return cls(session.location)
def _count(self):
counts = {}
with self.apt:
for source in self.apt.iter_sources():
for field in ['Build-Depends', 'Build-Depends-Indep',
'Build-Depends-Arch']:
for r in PkgRelation.parse_relations(
source.get(field, '')):
for p in r:
counts.setdefault(p['name'], 0)
counts[p['name']] += 1
import apt_pkg
apt_pkg.init()
apt_pkg.config.set("Dir", self.rootdir)
apt_cache = apt_pkg.SourceRecords()
apt_cache.restart()
while apt_cache.step():
try:
for d in apt_cache.build_depends.values():
for o in d:
for p in o:
counts.setdefault(p[0], 0)
counts[p[0]] += 1
except AttributeError:
pass
return counts
def __call__(self, reqs):
if self._counts is None:
try:
self._counts = self._count()
except NoAptSources:
logging.warning(
"No 'deb-src' in sources.list, "
"unable to break build-depends")
return None
self._counts = self._count()
by_count = {}
for req in reqs:
try:
@ -83,5 +80,5 @@ if __name__ == "__main__":
parser.add_argument("req", nargs="+")
args = parser.parse_args()
reqs = [AptRequirement.from_str(req) for req in args.req]
tie_breaker = BuildDependencyTieBreaker(LocalApt())
tie_breaker = BuildDependencyTieBreaker("/")
print(tie_breaker(reqs))

View file

@ -17,13 +17,12 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import apt_pkg
import asyncio
from datetime import datetime
from debian.deb822 import Release
import os
import re
import subprocess
from typing import List, AsyncIterator
from typing import Iterator, List
import logging
@ -33,15 +32,11 @@ from ..session import Session
class FileSearcher(object):
def search_files(
self, path: str, regex: bool = False,
case_insensitive: bool = False) -> AsyncIterator[str]:
self, path: str, regex: bool = False, case_insensitive: bool = False
) -> Iterator[str]:
raise NotImplementedError(self.search_files)
class AptFileAccessError(Exception):
"""Apt file access error."""
class ContentsFileNotFound(Exception):
"""The contents file was not found."""
@ -76,8 +71,7 @@ def contents_urls_from_sources_entry(source, arches, load_url):
response = load_url(release_url)
except FileNotFoundError as e:
logging.warning(
"Unable to download %s or %s: %s", inrelease_url,
release_url, e
"Unable to download %s or %s: %s", inrelease_url, release_url, e
)
return
@ -124,7 +118,7 @@ def _unwrap(f, ext):
def load_direct_url(url):
from urllib.error import HTTPError, URLError
from urllib.error import HTTPError
from urllib.request import urlopen, Request
for ext in [".xz", ".gz", ""]:
@ -134,11 +128,7 @@ def load_direct_url(url):
except HTTPError as e:
if e.status == 404:
continue
raise AptFileAccessError(
'Unable to access apt URL %s: %s' % (url + ext, e))
except URLError as e:
raise AptFileAccessError(
'Unable to access apt URL %s: %s' % (url + ext, e))
raise
break
else:
raise FileNotFoundError(url)
@ -197,7 +187,7 @@ class AptFileFileSearcher(FileSearcher):
@classmethod
def from_session(cls, session):
logging.debug('Using apt-file to search apt contents')
logging.info('Using apt-file to search apt contents')
if not os.path.exists(session.external_path(cls.CACHE_IS_EMPTY_PATH)):
from .apt import AptManager
AptManager.from_session(session).install(['apt-file'])
@ -205,7 +195,7 @@ class AptFileFileSearcher(FileSearcher):
session.check_call(['apt-file', 'update'], user='root')
return cls(session)
async def search_files(self, path, regex=False, case_insensitive=False):
def search_files(self, path, regex=False, case_insensitive=False):
args = []
if regex:
args.append('-x')
@ -214,17 +204,15 @@ class AptFileFileSearcher(FileSearcher):
if case_insensitive:
args.append('-i')
args.append(path)
process = await asyncio.create_subprocess_exec(
'/usr/bin/apt-file', 'search', *args,
stdout=asyncio.subprocess.PIPE)
(output, error) = await process.communicate(input=None)
if process.returncode == 1:
# No results
return
elif process.returncode == 3:
raise Exception('apt-file cache is empty')
elif process.returncode != 0:
raise Exception("unexpected return code %d" % process.returncode)
try:
output = self.session.check_output(['/usr/bin/apt-file', 'search'] + args)
except subprocess.CalledProcessError as e:
if e.returncode == 1:
# No results
return
if e.returncode == 3:
raise Exception('apt-file cache is empty')
raise
for line in output.splitlines(False):
pkg, path = line.split(b': ')
@ -265,8 +253,7 @@ class RemoteContentsFileSearcher(FileSearcher):
return load_url_with_cache(url, cache_dirs)
urls = list(
contents_urls_from_sourceslist(
sl, get_build_architecture(), load_url)
contents_urls_from_sourceslist(sl, get_build_architecture(), load_url)
)
self._load_urls(urls, cache_dirs, load_url)
@ -290,8 +277,8 @@ class RemoteContentsFileSearcher(FileSearcher):
return load_url_with_cache(url, cache_dirs)
urls = list(
contents_urls_from_sourceslist(
sl, get_build_architecture(), load_url))
contents_urls_from_sourceslist(sl, get_build_architecture(), load_url)
)
self._load_urls(urls, cache_dirs, load_url)
def _load_urls(self, urls, cache_dirs, load_url):
@ -299,16 +286,13 @@ class RemoteContentsFileSearcher(FileSearcher):
try:
f = load_url(url)
self.load_file(f, url)
except ConnectionResetError:
logging.warning("Connection reset error retrieving %s", url)
# TODO(jelmer): Retry?
except ContentsFileNotFound:
logging.warning("Unable to fetch contents file %s", url)
def __setitem__(self, path, package):
self._db[path] = package
async def search_files(self, path, regex=False, case_insensitive=False):
def search_files(self, path, regex=False, case_insensitive=False):
path = path.lstrip("/").encode("utf-8", "surrogateescape")
if case_insensitive and not regex:
regex = True
@ -354,9 +338,9 @@ class GeneratedFileSearcher(FileSearcher):
(path, pkg) = line.strip().split(None, 1)
self._db.append(path, pkg)
async def search_files(
self, path: str, regex: bool = False,
case_insensitive: bool = False):
def search_files(
self, path: str, regex: bool = False, case_insensitive: bool = False
) -> Iterator[str]:
for p, pkg in self._db:
if regex:
flags = 0
@ -387,17 +371,16 @@ GENERATED_FILE_SEARCHER = GeneratedFileSearcher(
)
async def get_packages_for_paths(
def get_packages_for_paths(
paths: List[str],
searchers: List[FileSearcher],
regex: bool = False,
case_insensitive: bool = False,
) -> List[str]:
candidates: List[str] = list()
# TODO(jelmer): Combine these, perhaps by creating one gigantic regex?
for path in paths:
for searcher in searchers:
async for pkg in searcher.search_files(
for pkg in searcher.search_files(
path, regex=regex, case_insensitive=case_insensitive
):
if pkg not in candidates:
@ -410,10 +393,8 @@ def main(argv):
from ..session.plain import PlainSession
parser = argparse.ArgumentParser()
parser.add_argument(
"path", help="Path to search for.", type=str, nargs="*")
parser.add_argument(
"--regex", "-x", help="Search for regex.", action="store_true")
parser.add_argument("path", help="Path to search for.", type=str, nargs="*")
parser.add_argument("--regex", "-x", help="Search for regex.", action="store_true")
parser.add_argument("--debug", action="store_true")
args = parser.parse_args()
@ -422,14 +403,13 @@ def main(argv):
else:
logging.basicConfig(level=logging.INFO)
with PlainSession() as session:
main_searcher = get_apt_contents_file_searcher(session)
searchers = [main_searcher, GENERATED_FILE_SEARCHER]
main_searcher = get_apt_contents_file_searcher(PlainSession())
main_searcher.load_local()
searchers = [main_searcher, GENERATED_FILE_SEARCHER]
packages = asyncio.run(get_packages_for_paths(
args.path, searchers=searchers, regex=args.regex))
for package in packages:
print(package)
packages = get_packages_for_paths(args.path, searchers=searchers, regex=args.regex)
for package in packages:
print(package)
if __name__ == "__main__":

View file

@ -22,10 +22,10 @@ __all__ = [
from functools import partial
import logging
import os
import re
import shutil
import sys
import time
from typing import List, Set, Optional, Type, Tuple
from typing import List, Set, Optional, Type
from debian.deb822 import (
Deb822,
@ -34,8 +34,6 @@ from debian.deb822 import (
from breezy.commit import PointlessCommit, NullCommitReporter
from breezy.tree import Tree
from breezy.workingtree import WorkingTree
from debmutate.changelog import ChangelogEditor
from debmutate.control import (
ensure_relation,
@ -52,7 +50,49 @@ from debmutate.reformatting import (
GeneratedFile,
)
from breezy.workspace import reset_tree
try:
from breezy.workspace import reset_tree
except ImportError: # breezy < 3.2
def delete_items(deletables, dry_run=False):
"""Delete files in the deletables iterable"""
import errno
import shutil
def onerror(function, path, excinfo):
"""Show warning for errors seen by rmtree."""
# Handle only permission error while removing files.
# Other errors are re-raised.
if function is not os.remove or excinfo[1].errno != errno.EACCES:
raise
logging.warning("unable to remove %s" % path)
for path, subp in deletables:
if os.path.isdir(path):
shutil.rmtree(path, onerror=onerror)
else:
try:
os.unlink(path)
except OSError as e:
# We handle only permission error here
if e.errno != errno.EACCES:
raise e
logging.warning('unable to remove "%s": %s.', path, e.strerror)
def reset_tree(local_tree, subpath=""):
from breezy.transform import revert
from breezy.clean_tree import iter_deletables
revert(
local_tree,
local_tree.branch.basis_tree(),
[subpath] if subpath not in (".", "") else None,
)
deletables = list(
iter_deletables(local_tree, unknown=True, ignored=False, detritus=False)
)
delete_items(deletables)
from debmutate._rules import (
dh_invoke_add_with,
@ -73,21 +113,18 @@ from buildlog_consultant.common import (
)
from buildlog_consultant.sbuild import (
DebcargoUnacceptablePredicate,
DebcargoUnacceptableComparator,
)
from .build import (
DetailedDebianBuildFailure,
UnidentifiedDebianBuildError,
)
from ..logs import rotate_logfile
from ..buildlog import problem_to_upstream_requirement
from ..fix_build import BuildFixer, resolve_error
from ..resolver.apt import (
AptRequirement,
)
from .apt import AptManager
from .build import attempt_build, DEFAULT_BUILDER, BUILD_LOG_FILENAME
from .build import attempt_build, DEFAULT_BUILDER
DEFAULT_MAX_ITERATIONS = 10
@ -113,9 +150,7 @@ class DebianPackagingContext(object):
def abspath(self, *parts):
return self.tree.abspath(os.path.join(self.subpath, *parts))
def commit(
self, summary: str,
update_changelog: Optional[bool] = None) -> bool:
def commit(self, summary: str, update_changelog: Optional[bool] = None) -> bool:
if update_changelog is None:
update_changelog = self.update_changelog
with self.tree.lock_write():
@ -179,11 +214,6 @@ def add_dependency(context, phase, requirement: AptRequirement):
return add_test_dependency(context, phase[1], requirement)
elif phase[0] == "build":
return add_build_dependency(context, requirement)
elif phase[0] == "buildenv":
# TODO(jelmer): Actually, we probably just want to install it on the
# host system?
logging.warning("Unknown phase %r", phase)
return False
else:
logging.warning("Unknown phase %r", phase)
return False
@ -201,19 +231,16 @@ def add_build_dependency(context, requirement: AptRequirement):
raise CircularDependency(binary["Package"])
for rel in requirement.relations:
updater.source["Build-Depends"] = ensure_relation(
updater.source.get("Build-Depends", ""),
PkgRelation.str([rel])
updater.source.get("Build-Depends", ""), PkgRelation.str([rel])
)
except FormattingUnpreservable as e:
logging.info(
"Unable to edit %s in a way that preserves formatting.", e.path)
logging.info("Unable to edit %s in a way that preserves formatting.", e.path)
return False
desc = requirement.pkg_relation_str()
if not updater.changed:
logging.info(
"Giving up; build dependency %s was already present.", desc)
logging.info("Giving up; dependency %s was already present.", desc)
return False
logging.info("Adding build dependency: %s", desc)
@ -245,18 +272,13 @@ def add_test_dependency(context, testname, requirement):
control.get("Depends", ""), PkgRelation.str([rel])
)
except FormattingUnpreservable as e:
logging.info(
"Unable to edit %s in a way that preserves formatting.", e.path)
logging.info("Unable to edit %s in a way that preserves formatting.", e.path)
return False
if not updater.changed:
return False
desc = requirement.pkg_relation_str()
if not updater.changed:
logging.info(
"Giving up; dependency %s for test %s was already present.",
desc, testname)
return False
logging.info("Adding dependency to test %s: %s", testname, desc)
return context.commit(
"Add missing dependency for test %s on %s." % (testname, desc),
@ -266,8 +288,7 @@ def add_test_dependency(context, testname, requirement):
def targeted_python_versions(tree: Tree, subpath: str) -> List[str]:
with tree.get_file(os.path.join(subpath, "debian/control")) as f:
control = Deb822(f)
build_depends = PkgRelation.parse_relations(
control.get("Build-Depends", ""))
build_depends = PkgRelation.parse_relations(control.get("Build-Depends", ""))
all_build_deps: Set[str] = set()
for or_deps in build_depends:
all_build_deps.update(or_dep["name"] for or_dep in or_deps)
@ -291,7 +312,7 @@ def python_tie_breaker(tree, subpath, reqs):
return True
if pkg.startswith("lib%s-" % python_version):
return True
if pkg == r'lib%s-dev' % python_version:
if re.match(r'lib%s\.[0-9]-dev' % python_version, pkg):
return True
return False
@ -316,8 +337,7 @@ def retry_apt_failure(error, phase, apt, context):
def enable_dh_autoreconf(context, phase):
# Debhelper >= 10 depends on dh-autoreconf and enables autoreconf by
# default.
debhelper_compat_version = get_debhelper_compat_level(
context.tree.abspath("."))
debhelper_compat_version = get_debhelper_compat_level(context.tree.abspath("."))
if debhelper_compat_version is not None and debhelper_compat_version < 10:
def add_with_autoreconf(line, target):
@ -336,8 +356,9 @@ def enable_dh_autoreconf(context, phase):
def fix_missing_configure(error, phase, context):
if (not context.tree.has_filename("configure.ac")
and not context.tree.has_filename("configure.in")):
if not context.tree.has_filename("configure.ac") and not context.tree.has_filename(
"configure.in"
):
return False
return enable_dh_autoreconf(context, phase)
@ -412,7 +433,7 @@ def fix_missing_makefile_pl(error, phase, context):
return False
def debcargo_coerce_unacceptable_prerelease(error, phase, context):
def coerce_unacceptable_predicate(error, phase, context):
from debmutate.debcargo import DebcargoEditor
with DebcargoEditor(context.abspath('debian/debcargo.toml')) as editor:
editor['allow_prerelease_deps'] = True
@ -440,8 +461,7 @@ class SimpleBuildFixer(BuildFixer):
class DependencyBuildFixer(BuildFixer):
def __init__(self, packaging_context, apt_resolver,
problem_cls: Type[Problem], fn):
def __init__(self, packaging_context, apt_resolver, problem_cls: Type[Problem], fn):
self.context = packaging_context
self.apt_resolver = apt_resolver
self._problem_cls = problem_cls
@ -461,47 +481,32 @@ class DependencyBuildFixer(BuildFixer):
return self._fn(problem, phase, self.apt_resolver, self.context)
def versioned_package_fixers(session, packaging_context, apt: AptManager):
def versioned_package_fixers(session, packaging_context, apt):
return [
PgBuildExtOutOfDateControlFixer(packaging_context, session, apt),
SimpleBuildFixer(
packaging_context, MissingConfigure, fix_missing_configure),
SimpleBuildFixer(packaging_context, MissingConfigure, fix_missing_configure),
SimpleBuildFixer(
packaging_context, MissingAutomakeInput, fix_missing_automake_input
),
SimpleBuildFixer(
packaging_context, MissingConfigStatusInput,
fix_missing_config_status_input
packaging_context, MissingConfigStatusInput, fix_missing_config_status_input
),
SimpleBuildFixer(
packaging_context, MissingPerlFile, fix_missing_makefile_pl),
SimpleBuildFixer(
packaging_context, DebcargoUnacceptablePredicate,
debcargo_coerce_unacceptable_prerelease),
SimpleBuildFixer(
packaging_context, DebcargoUnacceptableComparator,
debcargo_coerce_unacceptable_prerelease),
SimpleBuildFixer(packaging_context, MissingPerlFile, fix_missing_makefile_pl),
SimpleBuildFixer(packaging_context, DebcargoUnacceptablePredicate, coerce_unacceptable_predicate),
]
def apt_fixers(apt: AptManager, packaging_context,
dep_server_url: Optional[str] = None) -> List[BuildFixer]:
def apt_fixers(apt, packaging_context) -> List[BuildFixer]:
from ..resolver.apt import AptResolver
from .udd import popcon_tie_breaker
from .build_deps import BuildDependencyTieBreaker
apt_tie_breakers = [
partial(python_tie_breaker, packaging_context.tree,
packaging_context.subpath),
partial(python_tie_breaker, packaging_context.tree, packaging_context.subpath),
BuildDependencyTieBreaker.from_session(apt.session),
popcon_tie_breaker,
]
resolver: AptResolver
if dep_server_url:
from ..resolver.dep_server import DepServerAptResolver
resolver = DepServerAptResolver(apt, dep_server_url, apt_tie_breakers)
else:
resolver = AptResolver(apt, apt_tie_breakers)
resolver = AptResolver(apt, apt_tie_breakers)
return [
DependencyBuildFixer(
packaging_context, apt, AptFetchFailure, retry_apt_failure
@ -510,49 +515,38 @@ def apt_fixers(apt: AptManager, packaging_context,
]
def default_fixers(
local_tree: WorkingTree,
subpath: str, apt: AptManager,
committer: Optional[str] = None,
update_changelog: Optional[bool] = None,
dep_server_url: Optional[str] = None):
def default_fixers(local_tree, subpath, apt, committer=None, update_changelog=None):
packaging_context = DebianPackagingContext(
local_tree, subpath, committer, update_changelog,
commit_reporter=NullCommitReporter()
)
return (versioned_package_fixers(apt.session, packaging_context, apt)
+ apt_fixers(apt, packaging_context, dep_server_url))
return versioned_package_fixers(apt.session, packaging_context, apt) + apt_fixers(
apt, packaging_context
)
def build_incrementally(
local_tree: WorkingTree,
apt: AptManager,
suffix: str,
build_suite: str,
output_directory: str,
build_command: str,
local_tree,
apt,
suffix,
build_suite,
output_directory,
build_command,
build_changelog_entry,
committer: Optional[str] = None,
max_iterations: int = DEFAULT_MAX_ITERATIONS,
subpath: str = "",
committer=None,
max_iterations=DEFAULT_MAX_ITERATIONS,
subpath="",
source_date_epoch=None,
update_changelog: bool = True,
apt_repository: Optional[str] = None,
apt_repository_key: Optional[str] = None,
extra_repositories: Optional[List[str]] = None,
fixers: Optional[List[BuildFixer]] = None,
run_gbp_dch: Optional[bool] = None,
dep_server_url: Optional[str] = None,
update_changelog=True,
extra_repositories=None,
fixers=None
):
fixed_errors: List[Tuple[Problem, str]] = []
fixed_errors = []
if fixers is None:
fixers = default_fixers(
local_tree, subpath, apt, committer=committer,
update_changelog=update_changelog,
dep_server_url=dep_server_url)
update_changelog=update_changelog)
logging.info("Using fixers: %r", fixers)
if run_gbp_dch is None:
run_gbp_dch = (update_changelog is False)
while True:
try:
return attempt_build(
@ -564,9 +558,7 @@ def build_incrementally(
build_changelog_entry,
subpath=subpath,
source_date_epoch=source_date_epoch,
run_gbp_dch=run_gbp_dch,
apt_repository=apt_repository,
apt_repository_key=apt_repository_key,
run_gbp_dch=(update_changelog is False),
extra_repositories=extra_repositories,
)
except UnidentifiedDebianBuildError:
@ -577,19 +569,15 @@ def build_incrementally(
logging.info("No relevant context, not making any changes.")
raise
if (e.error, e.phase) in fixed_errors:
logging.warning(
"Error was still not fixed on second try. Giving up.")
logging.warning("Error was still not fixed on second try. Giving up.")
raise
if (max_iterations is not None
and len(fixed_errors) > max_iterations):
logging.warning(
"Last fix did not address the issue. Giving up.")
if max_iterations is not None and len(fixed_errors) > max_iterations:
logging.warning("Last fix did not address the issue. Giving up.")
raise
reset_tree(local_tree, subpath=subpath)
try:
if not resolve_error(e.error, e.phase, fixers):
logging.warning(
"Failed to resolve error %r. Giving up.", e.error)
logging.warning("Failed to resolve error %r. Giving up.", e.error)
raise
except GeneratedFile:
logging.warning(
@ -600,71 +588,71 @@ def build_incrementally(
raise e
except CircularDependency:
logging.warning(
"Unable to fix %r; it would introduce a circular "
"dependency.",
"Unable to fix %r; it would introduce a circular " "dependency.",
e.error,
)
raise e
fixed_errors.append((e.error, e.phase))
rotate_logfile(os.path.join(output_directory, BUILD_LOG_FILENAME))
if os.path.exists(os.path.join(output_directory, "build.log")):
i = 1
while os.path.exists(
os.path.join(output_directory, "build.log.%d" % i)
):
i += 1
target_path = os.path.join(output_directory, "build.log.%d" % i)
os.rename(os.path.join(output_directory, "build.log"), target_path)
logging.debug("Storing build log at %s", target_path)
def main(argv=None):
import argparse
parser = argparse.ArgumentParser("ognibuild.debian.fix_build")
modifications = parser.add_argument_group('Modifications')
modifications.add_argument(
"--suffix", type=str, help="Suffix to use for test builds.",
default="fixbuild1"
parser.add_argument(
"--suffix", type=str, help="Suffix to use for test builds.", default="fixbuild1"
)
modifications.add_argument(
parser.add_argument(
"--suite", type=str, help="Suite to target.", default="unstable"
)
modifications.add_argument(
"--committer", type=str, help="Committer string (name and email)",
default=None
parser.add_argument(
"--output-directory", type=str, help="Output directory.", default=None
)
modifications.add_argument(
parser.add_argument(
"--committer", type=str, help="Committer string (name and email)", default=None
)
parser.add_argument(
"--build-command",
type=str,
help="Build command",
default=(DEFAULT_BUILDER + " -A -s -v"),
)
parser.add_argument(
"--no-update-changelog",
action="store_false",
default=None,
dest="update_changelog",
help="do not update the changelog",
)
modifications.add_argument(
parser.add_argument(
'--max-iterations',
type=int,
default=DEFAULT_MAX_ITERATIONS,
help='Maximum number of issues to attempt to fix before giving up.')
parser.add_argument(
"--update-changelog",
action="store_true",
dest="update_changelog",
help="force updating of the changelog",
default=None,
)
build_behaviour = parser.add_argument_group('Build Behaviour')
build_behaviour.add_argument(
"--output-directory", type=str, help="Output directory.", default=None
)
build_behaviour.add_argument(
"--build-command",
type=str,
help="Build command",
default=(DEFAULT_BUILDER + " -A -s -v"),
)
build_behaviour.add_argument(
'--max-iterations',
type=int,
default=DEFAULT_MAX_ITERATIONS,
help='Maximum number of issues to attempt to fix before giving up.')
build_behaviour.add_argument("--schroot", type=str, help="chroot to use.")
parser.add_argument(
"--dep-server-url", type=str,
help="ognibuild dep server to use",
default=os.environ.get('OGNIBUILD_DEPS'))
parser.add_argument("--schroot", type=str, help="chroot to use.")
parser.add_argument("--verbose", action="store_true", help="Be verbose")
args = parser.parse_args()
from breezy.workingtree import WorkingTree
import breezy.git # noqa: F401
import breezy.bzr # noqa: F401
from .apt import AptManager
from ..session.plain import PlainSession
from ..session.schroot import SchrootSession
import tempfile
@ -681,10 +669,6 @@ def main(argv=None):
logging.info("Using output directory %s", output_directory)
else:
output_directory = args.output_directory
if not os.path.isdir(output_directory):
parser.error(
'output directory %s is not a directory'
% output_directory)
tree = WorkingTree.open(".")
if args.schroot:
@ -708,7 +692,6 @@ def main(argv=None):
committer=args.committer,
update_changelog=args.update_changelog,
max_iterations=args.max_iterations,
dep_server_url=args.dep_server_url,
)
except DetailedDebianBuildFailure as e:
if e.phase is None:
@ -718,21 +701,6 @@ def main(argv=None):
else:
phase = "%s (%s)" % (e.phase[0], e.phase[1])
logging.fatal("Error during %s: %s", phase, e.error)
if not args.output_directory:
xdg_cache_dir = os.environ.get(
'XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
buildlogs_dir = os.path.join(
xdg_cache_dir, 'ognibuild', 'buildlogs')
os.makedirs(buildlogs_dir, exist_ok=True)
target_log_file = os.path.join(
buildlogs_dir,
'%s-%s.log' % (
os.path.basename(getattr(tree, 'basedir', 'build')),
time.strftime('%Y-%m-%d_%H%M%s')))
shutil.copy(
os.path.join(output_directory, 'build.log'),
target_log_file)
logging.info('Build log available in %s', target_log_file)
return 1
except UnidentifiedDebianBuildError as e:
if e.phase is None:

View file

@ -35,8 +35,7 @@ class UDD(object):
def get_most_popular(self, packages):
cursor = self._conn.cursor()
cursor.execute(
"SELECT package FROM popcon "
"WHERE package IN %s ORDER BY insts DESC LIMIT 1",
"SELECT package FROM popcon WHERE package IN %s ORDER BY insts DESC LIMIT 1",
(tuple(packages),),
)
return cursor.fetchone()[0]
@ -55,8 +54,7 @@ def popcon_tie_breaker(candidates):
names = {list(c.package_names())[0]: c for c in candidates}
winner = udd.get_most_popular(list(names.keys()))
if winner is None:
logging.warning(
"No relevant popcon information found, not ranking by popcon")
logging.warning("No relevant popcon information found, not ranking by popcon")
return None
logging.info("Picked winner using popcon")
return names[winner]

View file

@ -1,126 +0,0 @@
#!/usr/bin/python
# Copyright (C) 2022 Jelmer Vernooij <jelmer@jelmer.uk>
# encoding: utf-8
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import logging
import sys
from aiohttp import web
from aiohttp_openmetrics import setup_metrics
from . import Requirement, UnknownRequirementFamily
from .debian.apt import AptManager
from .resolver.apt import resolve_requirement_apt
SUPPORTED_RELEASES = ['unstable', 'sid']
routes = web.RouteTableDef()
@routes.get('/health', name='health')
async def handle_health(request):
return web.Response(text='ok')
@routes.get('/families', name='families')
async def handle_families(request):
return web.json_response(list(Requirement._JSON_DESERIALIZERS.keys()))
@routes.post('/resolve-apt', name='resolve-apt')
async def handle_apt(request):
js = await request.json()
try:
req_js = js['requirement']
except KeyError:
raise web.HTTPBadRequest(text="json missing 'requirement' key")
release = js.get('release')
if release and release not in SUPPORTED_RELEASES:
return web.json_response(
{"reason": "unsupported-release", "release": release},
status=404)
try:
req = Requirement.from_json(req_js)
except UnknownRequirementFamily as e:
return web.json_response(
{"reason": "family-unknown", "family": e.family}, status=404)
apt_reqs = await resolve_requirement_apt(request.app['apt_mgr'], req)
return web.json_response([r.pkg_relation_str() for r in apt_reqs])
@routes.get('/resolve-apt/{release}/{family}:{arg}', name='resolve-apt-simple')
async def handle_apt_simple(request):
if request.match_info['release'] not in SUPPORTED_RELEASES:
return web.json_response(
{"reason": "unsupported-release",
"release": request.match_info['release']},
status=404)
try:
req = Requirement.from_json(
(request.match_info['family'], request.match_info['arg']))
except UnknownRequirementFamily as e:
return web.json_response(
{"reason": "family-unknown", "family": e.family}, status=404)
apt_reqs = await resolve_requirement_apt(request.app['apt_mgr'], req)
return web.json_response([r.pkg_relation_str() for r in apt_reqs])
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--listen-address', type=str, help='Listen address')
parser.add_argument('--schroot', type=str, help='Schroot session to use')
parser.add_argument('--port', type=str, help='Listen port', default=9934)
parser.add_argument('--debug', action='store_true')
parser.add_argument(
"--gcp-logging", action='store_true', help='Use Google cloud logging.')
args = parser.parse_args()
if args.gcp_logging:
import google.cloud.logging
client = google.cloud.logging.Client()
client.get_default_handler()
client.setup_logging()
else:
if args.debug:
log_level = logging.DEBUG
else:
log_level = logging.INFO
logging.basicConfig(
level=log_level,
format="[%(asctime)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S")
if args.schroot:
from .session.schroot import SchrootSession
session = SchrootSession(args.schroot)
else:
from .session.plain import PlainSession
session = PlainSession()
with session:
app = web.Application()
app.router.add_routes(routes)
app['apt_mgr'] = AptManager.from_session(session)
setup_metrics(app)
web.run_app(app, host=args.listen_address, port=args.port)
if __name__ == '__main__':
sys.exit(main())

View file

@ -18,19 +18,18 @@
__all__ = [
"UnidentifiedError",
"DetailedFailure",
"run_dist",
"create_dist_schroot",
"create_dist",
"dist",
"create_dist_schroot",
]
import errno
from functools import partial
import logging
import os
import sys
from typing import Optional, List
from debian.deb822 import Deb822
from breezy.tree import Tree
from breezy.workingtree import WorkingTree
@ -38,78 +37,16 @@ from buildlog_consultant.common import (
NoSpaceOnDevice,
)
from debian.deb822 import Deb822
from . import DetailedFailure, UnidentifiedError
from .dist_catcher import DistNoTarball
from .fix_build import iterate_with_build_fixers
from .logs import LogManager, NoLogManager
from .buildsystem import NoBuildToolsFound
from .resolver import auto_resolver
from .session import Session
from .session.schroot import SchrootSession
DIST_LOG_FILENAME = 'dist.log'
def run_dist(session, buildsystems, resolver, fixers, target_directory,
quiet=False, log_manager=None):
# Some things want to write to the user's home directory,
# e.g. pip caches in ~/.cache
session.create_home()
logging.info('Using dependency resolver: %s', resolver)
if log_manager is None:
log_manager = NoLogManager()
for buildsystem in buildsystems:
filename = iterate_with_build_fixers(fixers, log_manager.wrap(
partial(buildsystem.dist, session, resolver, target_directory,
quiet=quiet)))
return filename
raise NoBuildToolsFound()
def dist(session, export_directory, reldir, target_dir, log_manager, *,
version: Optional[str] = None, quiet=False):
from .fix_build import BuildFixer
from .buildsystem import detect_buildsystems
from .buildlog import InstallFixer
from .fixers import (
GitIdentityFixer,
MissingGoSumEntryFixer,
SecretGpgKeyFixer,
UnexpandedAutoconfMacroFixer,
GnulibDirectoryFixer,
)
if version:
# TODO(jelmer): Shouldn't include backend-specific code here
os.environ['SETUPTOOLS_SCM_PRETEND_VERSION'] = version
# TODO(jelmer): use scan_buildsystems to also look in subdirectories
buildsystems = list(detect_buildsystems(export_directory))
resolver = auto_resolver(session)
fixers: List[BuildFixer] = [
UnexpandedAutoconfMacroFixer(session, resolver),
GnulibDirectoryFixer(session),
MissingGoSumEntryFixer(session)]
fixers.append(InstallFixer(resolver))
if session.is_temporary:
# Only muck about with temporary sessions
fixers.extend([
GitIdentityFixer(session),
SecretGpgKeyFixer(session),
])
session.chdir(reldir)
def run_dist(session, buildsystems, resolver, fixers, target_directory, quiet=False):
# Some things want to write to the user's home directory,
# e.g. pip caches in ~/.cache
session.create_home()
@ -117,34 +54,31 @@ def dist(session, export_directory, reldir, target_dir, log_manager, *,
logging.info('Using dependency resolver: %s', resolver)
for buildsystem in buildsystems:
filename = iterate_with_build_fixers(fixers, log_manager.wrap(
partial(
buildsystem.dist, session, resolver, target_dir,
quiet=quiet)))
filename = buildsystem.dist(
session, resolver, fixers, target_directory, quiet=quiet
)
return filename
raise NoBuildToolsFound()
# This is the function used by debianize()
def create_dist(
session: Session,
tree: Tree,
target_dir: str,
include_controldir: bool = True,
subdir: Optional[str] = None,
log_manager: Optional[LogManager] = None,
version: Optional[str] = None,
cleanup: bool = False,
) -> Optional[str]:
"""Create a dist tarball for a tree.
from .buildsystem import detect_buildsystems
from .buildlog import InstallFixer
from .fix_build import BuildFixer
from .fixers import (
GitIdentityFixer,
SecretGpgKeyFixer,
UnexpandedAutoconfMacroFixer,
)
Args:
session: session to run it
tree: Tree object to work in
target_dir: Directory to write tarball into
include_controldir: Whether to include the version control directory
subdir: subdirectory in the tree to operate in
"""
if subdir is None:
subdir = "package"
try:
@ -156,11 +90,19 @@ def create_dist(
raise DetailedFailure(1, ["mkdtemp"], NoSpaceOnDevice())
raise
if log_manager is None:
log_manager = NoLogManager()
# TODO(jelmer): use scan_buildsystems to also look in subdirectories
buildsystems = list(detect_buildsystems(export_directory))
resolver = auto_resolver(session)
fixers: List[BuildFixer] = [UnexpandedAutoconfMacroFixer(session, resolver)]
return dist(session, export_directory, reldir, target_dir,
log_manager=log_manager, version=version)
fixers.append(InstallFixer(resolver))
if session.is_temporary:
# Only muck about with temporary sessions
fixers.extend([GitIdentityFixer(session), SecretGpgKeyFixer(session)])
session.chdir(reldir)
return run_dist(session, buildsystems, resolver, fixers, target_dir)
def create_dist_schroot(
@ -171,35 +113,30 @@ def create_dist_schroot(
packaging_subpath: Optional[str] = None,
include_controldir: bool = True,
subdir: Optional[str] = None,
log_manager: Optional[LogManager] = None,
cleanup: bool = False,
) -> Optional[str]:
"""Create a dist tarball for a tree.
Args:
session: session to run it
tree: Tree object to work in
target_dir: Directory to write tarball into
include_controldir: Whether to include the version control directory
subdir: subdirectory in the tree to operate in
"""
with SchrootSession(chroot) as session:
if packaging_tree is not None:
from .debian import satisfy_build_deps
satisfy_build_deps(session, packaging_tree, packaging_subpath)
return create_dist(
session, tree, target_dir,
include_controldir=include_controldir, subdir=subdir,
log_manager=log_manager)
session,
tree,
target_dir,
include_controldir=include_controldir,
subdir=subdir,
cleanup=cleanup,
)
def main(argv=None):
if __name__ == "__main__":
import argparse
import breezy.bzr # noqa: F401
import breezy.git # noqa: F401
from breezy.export import export
parser = argparse.ArgumentParser(argv)
parser = argparse.ArgumentParser()
parser.add_argument(
"--chroot",
default="unstable-amd64-sbuild",
@ -220,12 +157,8 @@ def main(argv=None):
"--target-directory", type=str, default="..", help="Target directory"
)
parser.add_argument("--verbose", action="store_true", help="Be verbose")
parser.add_argument("--mode", choices=["auto", "vcs", "buildsystem"],
type=str,
help="Mechanism to use to create buildsystem")
parser.add_argument(
"--include-controldir", action="store_true",
help="Clone rather than export."
"--include-controldir", action="store_true", help="Clone rather than export."
)
args = parser.parse_args()
@ -236,10 +169,6 @@ def main(argv=None):
logging.basicConfig(level=logging.INFO, format="%(message)s")
tree = WorkingTree.open(args.directory)
packaging_tree: Optional[WorkingTree]
subdir: Optional[str]
if args.packaging_directory:
packaging_tree = WorkingTree.open(args.packaging_directory)
with packaging_tree.lock_read():
@ -250,47 +179,30 @@ def main(argv=None):
packaging_tree = None
subdir = None
if args.mode == 'vcs':
try:
ret = create_dist_schroot(
tree,
subdir=subdir,
target_dir=os.path.abspath(args.target_directory),
packaging_tree=packaging_tree,
chroot=args.chroot,
include_controldir=args.include_controldir,
)
except (NoBuildToolsFound, NotImplementedError):
logging.info("No build tools found, falling back to simple export.")
export(tree, "dist.tar.gz", "tgz", None)
elif args.mode in ('auto', 'buildsystem'):
try:
ret = create_dist_schroot(
tree,
subdir=subdir,
target_dir=os.path.abspath(args.target_directory),
packaging_tree=packaging_tree,
chroot=args.chroot,
include_controldir=args.include_controldir,
)
except NoBuildToolsFound:
if args.mode == 'buildsystem':
logging.fatal('No build tools found, unable to create tarball')
return 1
logging.info(
"No build tools found, falling back to simple export.")
export(tree, "dist.tar.gz", "tgz", None)
except NotImplementedError:
if args.mode == 'buildsystem':
logging.fatal('Unable to ask buildsystem for tarball')
return 1
logging.info(
"Build system does not support dist tarball creation, "
"falling back to simple export."
)
export(tree, "dist.tar.gz", "tgz", None)
except UnidentifiedError as e:
logging.fatal("Unidentified error: %r", e.lines)
return 1
except DetailedFailure as e:
logging.fatal("Identified error during dist creation: %s", e.error)
return 1
except DistNoTarball:
logging.fatal("dist operation did not create a tarball")
return 1
else:
logging.info("Created %s", ret)
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
except NotImplementedError:
logging.info(
"Build system does not support dist tarball creation, "
"falling back to simple export."
)
export(tree, "dist.tar.gz", "tgz", None)
except UnidentifiedError as e:
logging.fatal("Unidentified error: %r", e.lines)
except DetailedFailure as e:
logging.fatal("Identified error during dist creation: %s", e.error)
except DistNoTarball:
logging.fatal("dist operation did not create a tarball")
else:
logging.info("Created %s", ret)
sys.exit(0)

View file

@ -54,8 +54,7 @@ class DistCatcher(object):
@classmethod
def default(cls, directory):
return cls(
[os.path.join(directory, "dist"), directory,
os.path.join(directory, "..")]
[os.path.join(directory, "dist"), directory, os.path.join(directory, "..")]
)
def __enter__(self):
@ -88,23 +87,19 @@ class DistCatcher(object):
continue
if len(possible_new) == 1:
entry = possible_new[0]
logging.info(
"Found new tarball %s in %s.", entry.name, directory)
logging.info("Found new tarball %s in %s.", entry.name, directory)
self.files.append(entry.path)
return entry.name
elif len(possible_new) > 1:
logging.warning(
"Found multiple tarballs %r in %s.", possible_new,
directory
"Found multiple tarballs %r in %s.", possible_new, directory
)
self.files.extend([entry.path for entry in possible_new])
return possible_new[0].name
if len(possible_updated) == 1:
entry = possible_updated[0]
logging.info(
"Found updated tarball %s in %s.", entry.name,
directory)
logging.info("Found updated tarball %s in %s.", entry.name, directory)
self.files.append(entry.path)
return entry.name

View file

@ -17,7 +17,7 @@
from functools import partial
import logging
from typing import List, Tuple, Callable, Optional, TypeVar
from typing import List, Tuple, Callable, Any, Optional
from buildlog_consultant import Problem
from buildlog_consultant.common import (
@ -29,14 +29,6 @@ from . import DetailedFailure, UnidentifiedError
from .session import Session, run_with_tee
# Number of attempts to fix a build before giving up.
DEFAULT_LIMIT = 200
class FixerLimitReached(Exception):
"""The maximum number of fixes has been reached."""
class BuildFixer(object):
"""Build fixer."""
@ -52,11 +44,7 @@ class BuildFixer(object):
return self._fix(problem, phase)
def run_detecting_problems(
session: Session, args: List[str], check_success=None,
quiet=False, **kwargs) -> List[str]:
if not quiet:
logging.info('Running %r', args)
def run_detecting_problems(session: Session, args: List[str], check_success=None, **kwargs):
if check_success is None:
def check_success(retcode, contents):
return (retcode == 0)
@ -75,26 +63,17 @@ def run_detecting_problems(
logging.warning("Build failed with unidentified error:")
logging.warning("%s", match.line.rstrip("\n"))
else:
logging.warning(
"Build failed and unable to find cause. Giving up.")
logging.warning("Build failed and unable to find cause. Giving up.")
raise UnidentifiedError(retcode, args, lines, secondary=match)
raise DetailedFailure(retcode, args, error)
T = TypeVar('T')
def iterate_with_build_fixers(
fixers: List[BuildFixer],
cb: Callable[[], T], limit=DEFAULT_LIMIT) -> T:
def iterate_with_build_fixers(fixers: List[BuildFixer], cb: Callable[[], Any]):
"""Call cb() until there are no more DetailedFailures we can fix.
Args:
fixers: List of fixers to use to resolve issues
cb: Callable to run the build
limit: Maximum number of fixing attempts before giving up
"""
attempts = 0
fixed_errors = []
while True:
to_resolve = []
@ -107,13 +86,9 @@ def iterate_with_build_fixers(
logging.info("Identified error: %r", f.error)
if f.error in fixed_errors:
logging.warning(
"Failed to resolve error %r, it persisted. Giving up.",
f.error
"Failed to resolve error %r, it persisted. Giving up.", f.error
)
raise f
attempts += 1
if limit is not None and limit <= attempts:
raise FixerLimitReached(limit)
try:
resolved = resolve_error(f.error, None, fixers=fixers)
except DetailedFailure as n:
@ -125,25 +100,23 @@ def iterate_with_build_fixers(
else:
if not resolved:
logging.warning(
"Failed to find resolution for error %r. Giving up.",
f.error
"Failed to find resolution for error %r. Giving up.", f.error
)
raise f
fixed_errors.append(f.error)
def run_with_build_fixers(
fixers: Optional[List[BuildFixer]], session: Session, args: List[str],
quiet: bool = False, **kwargs
) -> List[str]:
session: Session, args: List[str], fixers: Optional[List[BuildFixer]], **kwargs
):
if fixers is None:
fixers = []
return iterate_with_build_fixers(
fixers,
partial(run_detecting_problems, session, args, quiet=quiet, **kwargs))
fixers, partial(run_detecting_problems, session, args, **kwargs)
)
def resolve_error(error, phase, fixers) -> bool:
def resolve_error(error, phase, fixers):
relevant_fixers = []
for fixer in fixers:
if fixer.can_fix(error):

View file

@ -21,10 +21,8 @@ from typing import Tuple
from buildlog_consultant import Problem
from buildlog_consultant.common import (
MissingGitIdentity,
MissingGoSumEntry,
MissingSecretGpgKey,
MissingAutoconfMacro,
MissingGnulibDirectory,
)
from ognibuild.requirements import AutoconfMacroRequirement
from ognibuild.resolver import UnsatisfiedRequirements
@ -32,18 +30,6 @@ from ognibuild.resolver import UnsatisfiedRequirements
from .fix_build import BuildFixer
class GnulibDirectoryFixer(BuildFixer):
def __init__(self, session):
self.session = session
def can_fix(self, problem: Problem):
return isinstance(problem, MissingGnulibDirectory)
def _fix(self, problem: Problem, phase: Tuple[str, ...]):
self.session.check_call(["./gnulib.sh"])
return True
class GitIdentityFixer(BuildFixer):
def __init__(self, session):
self.session = session
@ -91,26 +77,6 @@ Passphrase: ""
return False
class MissingGoSumEntryFixer(BuildFixer):
def __init__(self, session):
self.session = session
def __repr__(self):
return "%s()" % (type(self).__name__)
def __str__(self):
return "missing go.sum entry fixer"
def can_fix(self, error):
return isinstance(error, MissingGoSumEntry)
def _fix(self, error, phase):
from .fix_build import run_detecting_problems
run_detecting_problems(
self.session, ["go", "mod", "download", error.package])
return True
class UnexpandedAutoconfMacroFixer(BuildFixer):
def __init__(self, session, resolver):
self.session = session

View file

@ -21,13 +21,11 @@ def run_info(session, buildsystems, fixers=None):
print("%r:" % buildsystem)
deps = {}
try:
for kind, dep in buildsystem.get_declared_dependencies(
session, fixers=fixers):
for kind, dep in buildsystem.get_declared_dependencies(session, fixers=fixers):
deps.setdefault(kind, []).append(dep)
except NotImplementedError:
print(
"\tUnable to detect declared dependencies for this type of "
"build system"
"\tUnable to detect declared dependencies for this type of build system"
)
if deps:
print("\tDeclared dependencies:")
@ -37,11 +35,9 @@ def run_info(session, buildsystems, fixers=None):
print("\t\t\t%s" % dep)
print("")
try:
outputs = list(buildsystem.get_declared_outputs(
session, fixers=fixers))
outputs = list(buildsystem.get_declared_outputs(session, fixers=fixers))
except NotImplementedError:
print("\tUnable to detect declared outputs for this type of "
"build system")
print("\tUnable to detect declared outputs for this type of build system")
outputs = []
if outputs:
print("\tDeclared outputs:")

View file

@ -15,34 +15,21 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from functools import partial
from .buildsystem import NoBuildToolsFound, InstallTarget
from typing import Optional
from .buildsystem import NoBuildToolsFound, InstallTarget
from .fix_build import iterate_with_build_fixers
from .logs import NoLogManager
def run_install(
session, buildsystems, resolver, fixers, *, user: bool = False,
prefix: Optional[str] = None, log_manager=None):
def run_install(session, buildsystems, resolver, fixers, user: bool = False, prefix: Optional[str] = None):
# Some things want to write to the user's home directory,
# e.g. pip caches in ~/.cache
session.create_home()
if log_manager is None:
log_manager = NoLogManager()
install_target = InstallTarget()
install_target.user = user
install_target.prefix = prefix
for buildsystem in buildsystems:
iterate_with_build_fixers(
fixers,
log_manager.wrap(
partial(buildsystem.install, session, resolver,
install_target)))
buildsystem.install(session, resolver, fixers, install_target)
return
raise NoBuildToolsFound()

View file

@ -1,105 +0,0 @@
#!/usr/bin/python
# Copyright (C) 2018 Jelmer Vernooij <jelmer@jelmer.uk>
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from contextlib import contextmanager
import subprocess
import logging
import os
import sys
@contextmanager
def copy_output(output_log: str, tee: bool = False):
old_stdout = os.dup(sys.stdout.fileno())
old_stderr = os.dup(sys.stderr.fileno())
if tee:
p = subprocess.Popen(["tee", output_log], stdin=subprocess.PIPE)
newfd = p.stdin
else:
newfd = open(output_log, 'wb')
os.dup2(newfd.fileno(), sys.stdout.fileno()) # type: ignore
os.dup2(newfd.fileno(), sys.stderr.fileno()) # type: ignore
try:
yield
finally:
sys.stdout.flush()
sys.stderr.flush()
os.dup2(old_stdout, sys.stdout.fileno())
os.dup2(old_stderr, sys.stderr.fileno())
if newfd is not None:
newfd.close()
@contextmanager
def redirect_output(to_file):
sys.stdout.flush()
sys.stderr.flush()
old_stdout = os.dup(sys.stdout.fileno())
old_stderr = os.dup(sys.stderr.fileno())
os.dup2(to_file.fileno(), sys.stdout.fileno()) # type: ignore
os.dup2(to_file.fileno(), sys.stderr.fileno()) # type: ignore
try:
yield
finally:
sys.stdout.flush()
sys.stderr.flush()
os.dup2(old_stdout, sys.stdout.fileno())
os.dup2(old_stderr, sys.stderr.fileno())
def rotate_logfile(source_path: str) -> None:
if os.path.exists(source_path):
(directory_path, name) = os.path.split(source_path)
i = 1
while os.path.exists(
os.path.join(directory_path, "%s.%d" % (name, i))):
i += 1
target_path = os.path.join(directory_path, "%s.%d" % (name, i))
os.rename(source_path, target_path)
logging.debug("Storing previous build log at %s", target_path)
class LogManager(object):
def wrap(self, fn):
raise NotImplementedError(self.wrap)
class DirectoryLogManager(LogManager):
def __init__(self, path, mode):
self.path = path
self.mode = mode
def wrap(self, fn):
def _run(*args, **kwargs):
rotate_logfile(self.path)
if self.mode == 'copy':
with copy_output(self.path, tee=True):
return fn(*args, **kwargs)
elif self.mode == 'redirect':
with copy_output(self.path, tee=False):
return fn(*args, **kwargs)
else:
raise NotImplementedError(self.mode)
return _run
class NoLogManager(LogManager):
def wrap(self, fn):
return fn

View file

@ -26,19 +26,16 @@ from . import Requirement
class PythonPackageRequirement(Requirement):
family = "python-package"
package: str
def __init__(
self, package, python_version=None, specs=None,
minimum_version=None):
def __init__(self, package, python_version=None, specs=None, minimum_version=None):
super(PythonPackageRequirement, self).__init__("python-package")
self.package = package
self.python_version = python_version
if minimum_version is not None:
specs = [(">=", minimum_version)]
if specs is None:
specs = []
if minimum_version is not None:
specs.append((">=", minimum_version))
self.specs = specs
def __repr__(self):
@ -56,29 +53,11 @@ class PythonPackageRequirement(Requirement):
return "python package: %s" % (self.package,)
@classmethod
def from_requirement_str(cls, text, python_version=None):
def from_requirement_str(cls, text):
from requirements.requirement import Requirement
req = Requirement.parse(text)
return cls(
package=req.name, specs=req.specs, python_version=python_version)
def requirement_str(self):
if self.specs:
return '%s;%s' % (
self.package, ','.join([''.join(s) for s in self.specs]))
return self.package
@classmethod
def _from_json(cls, js):
if isinstance(js, str):
return cls.from_requirement_str(js)
return cls.from_requirement_str(js[0], python_version=js[1])
def _json(self):
if self.python_version:
return [self.requirement_str(), self.python_version]
return self.requirement_str()
return cls(package=req.name, specs=req.specs)
def met(self, session):
if self.python_version == "cpython3":
@ -95,8 +74,7 @@ class PythonPackageRequirement(Requirement):
raise NotImplementedError
text = self.package + ",".join(["".join(spec) for spec in self.specs])
p = session.Popen(
[cmd, "-c",
"import pkg_resources; pkg_resources.require(%r)" % text],
[cmd, "-c", "import pkg_resources; pkg_resources.require(%r)" % text],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
@ -104,33 +82,16 @@ class PythonPackageRequirement(Requirement):
return p.returncode == 0
Requirement.register_json(PythonPackageRequirement)
class LatexPackageRequirement(Requirement):
family = "latex-package"
def __init__(self, package: str):
self.package = package
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.package)
def _json(self):
return self.package
def _from_json(cls, package):
return cls(package)
Requirement.register_json(LatexPackageRequirement)
class PhpPackageRequirement(Requirement):
family = "php-package"
def __init__(
self,
package: str,
@ -143,13 +104,6 @@ class PhpPackageRequirement(Requirement):
self.min_version = min_version
self.max_version = max_version
def _json(self):
return (self.package, self.channel, self.min_version, self.max_version)
@classmethod
def _from_json(cls, js):
return cls(*js)
def __repr__(self):
return "%s(%r, %r, %r, %r)" % (
type(self).__name__,
@ -160,24 +114,14 @@ class PhpPackageRequirement(Requirement):
)
Requirement.register_json(PhpPackageRequirement)
class BinaryRequirement(Requirement):
family = "binary"
binary_name: str
def __init__(self, binary_name):
super(BinaryRequirement, self).__init__("binary")
self.binary_name = binary_name
def _json(self):
return self.binary_name
@classmethod
def _from_json(cls, js):
return cls(js)
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.binary_name)
@ -191,54 +135,14 @@ class BinaryRequirement(Requirement):
return p.returncode == 0
Requirement.register_json(BinaryRequirement)
class PHPExtensionRequirement(Requirement):
family = "php-extension"
extension: str
def __init__(self, extension: str):
self.extension = extension
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.extension)
class PytestPluginRequirement(Requirement):
family = "pytest-plugin"
plugin: str
def __init__(self, plugin: str):
self.plugin = plugin
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.plugin)
class VcsControlDirectoryAccessRequirement(Requirement):
vcs: List[str]
family = "vcs-access"
def __init__(self, vcs):
self.vcs = vcs
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.vcs)
class PerlModuleRequirement(Requirement):
module: str
filename: Optional[str]
inc: Optional[List[str]]
family = "perl-module"
def __init__(self, module, filename=None, inc=None):
super(PerlModuleRequirement, self).__init__("perl-module")
self.module = module
self.filename = filename
self.inc = inc
@ -254,10 +158,10 @@ class PerlModuleRequirement(Requirement):
class VagueDependencyRequirement(Requirement):
name: str
family = "vague"
minimum_version: Optional[str] = None
def __init__(self, name, minimum_version=None):
super(VagueDependencyRequirement, self).__init__("vague")
self.name = name
self.minimum_version = minimum_version
@ -265,26 +169,19 @@ class VagueDependencyRequirement(Requirement):
if " " not in self.name:
yield BinaryRequirement(self.name)
yield LibraryRequirement(self.name)
yield PkgConfigRequirement(
self.name, minimum_version=self.minimum_version)
yield PkgConfigRequirement(self.name, minimum_version=self.minimum_version)
if self.name.lower() != self.name:
yield BinaryRequirement(self.name.lower())
yield LibraryRequirement(self.name.lower())
yield PkgConfigRequirement(
self.name.lower(), minimum_version=self.minimum_version)
try:
from .resolver.apt import AptRequirement
except ModuleNotFoundError:
pass
yield PkgConfigRequirement(self.name.lower(), minimum_version=self.minimum_version)
from .resolver.apt import AptRequirement
yield AptRequirement.simple(self.name.lower(), minimum_version=self.minimum_version)
if self.name.lower().startswith('lib'):
devname = '%s-dev' % self.name.lower()
else:
yield AptRequirement.simple(
self.name.lower(), minimum_version=self.minimum_version)
if self.name.lower().startswith('lib'):
devname = '%s-dev' % self.name.lower()
else:
devname = 'lib%s-dev' % self.name.lower()
yield AptRequirement.simple(
devname, minimum_version=self.minimum_version)
devname = 'lib%s-dev' % self.name.lower()
yield AptRequirement.simple(devname, minimum_version=self.minimum_version)
def met(self, session):
for x in self.expand():
@ -295,36 +192,19 @@ class VagueDependencyRequirement(Requirement):
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.name)
def __str__(self):
if self.minimum_version:
return "%s >= %s" % (self.name, self.minimum_version)
return self.name
class NodePackageRequirement(Requirement):
package: str
family = "npm-package"
def __init__(self, package):
super(NodePackageRequirement, self).__init__("npm-package")
self.package = package
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.package)
class LuaModuleRequirement(Requirement):
module: str
family = "lua-module"
def __init__(self, module):
self.module = module
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.module)
class PerlPreDeclaredRequirement(Requirement):
name: str
@ -347,9 +227,8 @@ class PerlPreDeclaredRequirement(Requirement):
'auto_set_bugtracker': 'Module::Install::Bugtracker',
}
family = "perl-predeclared"
def __init__(self, name):
super(PerlPreDeclaredRequirement, self).__init__("perl-predeclared")
self.name = name
def lookup_module(self):
@ -363,9 +242,9 @@ class PerlPreDeclaredRequirement(Requirement):
class NodeModuleRequirement(Requirement):
module: str
family = "npm-module"
def __init__(self, module):
super(NodeModuleRequirement, self).__init__("npm-module")
self.module = module
def __repr__(self):
@ -376,45 +255,41 @@ class CargoCrateRequirement(Requirement):
crate: str
features: Set[str]
api_version: Optional[str]
minimum_version: Optional[str]
family = "cargo-crate"
version: Optional[str]
def __init__(self, crate, features=None, api_version=None,
minimum_version=None):
def __init__(self, crate, features=None, version=None):
super(CargoCrateRequirement, self).__init__("cargo-crate")
self.crate = crate
if features is None:
features = set()
self.features = features
self.api_version = api_version
self.minimum_version = minimum_version
self.version = version
def __repr__(self):
return "%s(%r, features=%r, api_version=%r, minimum_version=%r)" % (
return "%s(%r, features=%r, version=%r)" % (
type(self).__name__,
self.crate,
self.features,
self.api_version,
self.minimum_version,
self.version,
)
def __str__(self):
ret = "cargo crate: %s %s" % (
self.crate,
self.api_version or "")
if self.features:
ret += " (%s)" % (", ".join(sorted(self.features)))
if self.minimum_version:
ret += " (>= %s)" % self.minimum_version
return ret
return "cargo crate: %s %s (%s)" % (
self.crate,
self.version or "",
", ".join(sorted(self.features)),
)
else:
return "cargo crate: %s %s" % (self.crate, self.version or "")
class PkgConfigRequirement(Requirement):
module: str
family = "pkg-config"
def __init__(self, module, minimum_version=None):
super(PkgConfigRequirement, self).__init__("pkg-config")
self.module = module
self.minimum_version = minimum_version
@ -426,9 +301,9 @@ class PkgConfigRequirement(Requirement):
class PathRequirement(Requirement):
path: str
family = "path"
def __init__(self, path):
super(PathRequirement, self).__init__("path")
self.path = path
def __repr__(self):
@ -438,9 +313,9 @@ class PathRequirement(Requirement):
class CHeaderRequirement(Requirement):
header: str
family = "c-header"
def __init__(self, header):
super(CHeaderRequirement, self).__init__("c-header")
self.header = header
def __repr__(self):
@ -448,15 +323,16 @@ class CHeaderRequirement(Requirement):
class JavaScriptRuntimeRequirement(Requirement):
family = "javascript-runtime"
def __init__(self):
super(JavaScriptRuntimeRequirement, self).__init__("javascript-runtime")
class ValaPackageRequirement(Requirement):
package: str
family = "vala"
def __init__(self, package: str):
super(ValaPackageRequirement, self).__init__("vala")
self.package = package
@ -464,9 +340,9 @@ class RubyGemRequirement(Requirement):
gem: str
minimum_version: Optional[str]
family = "gem"
def __init__(self, gem: str, minimum_version: Optional[str]):
super(RubyGemRequirement, self).__init__("gem")
self.gem = gem
self.minimum_version = minimum_version
@ -475,16 +351,12 @@ class GoPackageRequirement(Requirement):
package: str
version: Optional[str]
family = "go-package"
def __init__(self, package: str, version: Optional[str] = None):
super(GoPackageRequirement, self).__init__("go-package")
self.package = package
self.version = version
def __repr__(self):
return "%s(%r, version=%r)" % (
type(self).__name__, self.package, self.version)
def __str__(self):
if self.version:
return "go package: %s (= %s)" % (self.package, self.version)
@ -494,9 +366,9 @@ class GoPackageRequirement(Requirement):
class GoRequirement(Requirement):
version: Optional[str]
family = "go"
def __init__(self, version: Optional[str] = None):
super(GoRequirement, self).__init__("go")
self.version = version
def __str__(self):
@ -506,18 +378,18 @@ class GoRequirement(Requirement):
class DhAddonRequirement(Requirement):
path: str
family = "dh-addon"
def __init__(self, path: str):
super(DhAddonRequirement, self).__init__("dh-addon")
self.path = path
class PhpClassRequirement(Requirement):
php_class: str
family = "php-class"
def __init__(self, php_class: str):
super(PhpClassRequirement, self).__init__("php-class")
self.php_class = php_class
@ -525,9 +397,9 @@ class RPackageRequirement(Requirement):
package: str
minimum_version: Optional[str]
family = "r-package"
def __init__(self, package: str, minimum_version: Optional[str] = None):
super(RPackageRequirement, self).__init__("r-package")
self.package = package
self.minimum_version = minimum_version
@ -540,8 +412,7 @@ class RPackageRequirement(Requirement):
def __str__(self):
if self.minimum_version:
return "R package: %s (>= %s)" % (
self.package, self.minimum_version)
return "R package: %s (>= %s)" % (self.package, self.minimum_version)
else:
return "R package: %s" % (self.package,)
@ -561,9 +432,9 @@ class OctavePackageRequirement(Requirement):
package: str
minimum_version: Optional[str]
family = "octave-package"
def __init__(self, package: str, minimum_version: Optional[str] = None):
super(OctavePackageRequirement, self).__init__("octave-package")
self.package = package
self.minimum_version = minimum_version
@ -576,8 +447,7 @@ class OctavePackageRequirement(Requirement):
def __str__(self):
if self.minimum_version:
return "Octave package: %s (>= %s)" % (
self.package, self.minimum_version)
return "Octave package: %s (>= %s)" % (self.package, self.minimum_version)
else:
return "Octave package: %s" % (self.package,)
@ -596,9 +466,9 @@ class OctavePackageRequirement(Requirement):
class LibraryRequirement(Requirement):
library: str
family = "lib"
def __init__(self, library: str):
super(LibraryRequirement, self).__init__("lib")
self.library = library
@ -606,9 +476,9 @@ class StaticLibraryRequirement(Requirement):
library: str
filename: str
family = "static-lib"
def __init__(self, library: str, filename: str):
super(StaticLibraryRequirement, self).__init__("static-lib")
self.library = library
self.filename = filename
@ -616,18 +486,18 @@ class StaticLibraryRequirement(Requirement):
class RubyFileRequirement(Requirement):
filename: str
family = "ruby-file"
def __init__(self, filename: str):
super(RubyFileRequirement, self).__init__("ruby-file")
self.filename = filename
class XmlEntityRequirement(Requirement):
url: str
family = "xml-entity"
def __init__(self, url: str):
super(XmlEntityRequirement, self).__init__("xml-entity")
self.url = url
@ -635,9 +505,9 @@ class SprocketsFileRequirement(Requirement):
content_type: str
name: str
family = "sprockets-file"
def __init__(self, content_type: str, name: str):
super(SprocketsFileRequirement, self).__init__("sprockets-file")
self.content_type = content_type
self.name = name
@ -645,29 +515,27 @@ class SprocketsFileRequirement(Requirement):
class JavaClassRequirement(Requirement):
classname: str
family = "java-class"
def __init__(self, classname: str):
super(JavaClassRequirement, self).__init__("java-class")
self.classname = classname
class CMakefileRequirement(Requirement):
filename: str
version: Optional[str]
family = "cmake-file"
def __init__(self, filename: str, version=None):
def __init__(self, filename: str):
super(CMakefileRequirement, self).__init__("cmake-file")
self.filename = filename
self.version = version
class HaskellPackageRequirement(Requirement):
package: str
family = "haskell-package"
def __init__(self, package: str, specs=None):
super(HaskellPackageRequirement, self).__init__("haskell-package")
self.package = package
self.specs = specs
@ -683,9 +551,9 @@ class MavenArtifactRequirement(Requirement):
artifact_id: str
version: Optional[str]
kind: Optional[str]
family = "maven-artifact"
def __init__(self, group_id, artifact_id, version=None, kind=None):
super(MavenArtifactRequirement, self).__init__("maven-artifact")
self.group_id = group_id
self.artifact_id = artifact_id
self.version = version
@ -698,11 +566,6 @@ class MavenArtifactRequirement(Requirement):
self.version,
)
def __repr__(self):
return "%s(group_id=%r, artifact_id=%r, version=%r, kind=%r)" % (
type(self).__name__, self.group_id, self.artifact_id,
self.version, self.kind)
@classmethod
def from_str(cls, text):
return cls.from_tuple(text.split(":"))
@ -724,16 +587,17 @@ class MavenArtifactRequirement(Requirement):
class GnomeCommonRequirement(Requirement):
family = "gnome-common"
def __init__(self):
super(GnomeCommonRequirement, self).__init__("gnome-common")
class JDKFileRequirement(Requirement):
jdk_path: str
filename: str
family = "jdk-file"
def __init__(self, jdk_path: str, filename: str):
super(JDKFileRequirement, self).__init__("jdk-file")
self.jdk_path = jdk_path
self.filename = filename
@ -743,70 +607,55 @@ class JDKFileRequirement(Requirement):
class JDKRequirement(Requirement):
family = "jdk"
def __init__(self):
super(JDKRequirement, self).__init__("jdk")
class JRERequirement(Requirement):
family = "jre"
class QtModuleRequirement(Requirement):
family = "qt-module"
def __init__(self, module):
self.module = module
def __init__(self):
super(JRERequirement, self).__init__("jre")
class QTRequirement(Requirement):
family = "qt"
def __init__(self):
super(QTRequirement, self).__init__("qt")
class X11Requirement(Requirement):
family = "x11"
def __init__(self):
super(X11Requirement, self).__init__("x11")
class CertificateAuthorityRequirement(Requirement):
family = "ca-cert"
def __init__(self, url):
super(CertificateAuthorityRequirement, self).__init__("ca-cert")
self.url = url
class PerlFileRequirement(Requirement):
filename: str
family = "perl-file"
def __init__(self, filename: str):
super(PerlFileRequirement, self).__init__("perl-file")
self.filename = filename
class AutoconfMacroRequirement(Requirement):
family = "autoconf-macro"
macro: str
def __init__(self, macro: str):
super(AutoconfMacroRequirement, self).__init__("autoconf-macro")
self.macro = macro
def _json(self):
return self.macro
@classmethod
def _from_json(cls, macro):
return cls(macro)
Requirement.register_json(AutoconfMacroRequirement)
class LibtoolRequirement(Requirement):
family = "libtool"
def __init__(self):
super(LibtoolRequirement, self).__init__("libtool")
class IntrospectionTypelibRequirement(Requirement):
family = "introspection-type-lib"
def __init__(self, library):
self.library = library
@ -816,9 +665,9 @@ class PythonModuleRequirement(Requirement):
module: str
python_version: Optional[str]
minimum_version: Optional[str]
family = "python-module"
def __init__(self, module, python_version=None, minimum_version=None):
super(PythonModuleRequirement, self).__init__("python-module")
self.module = module
self.python_version = python_version
self.minimum_version = minimum_version
@ -853,25 +702,7 @@ class PythonModuleRequirement(Requirement):
class BoostComponentRequirement(Requirement):
name: str
family = "boost-component"
def __init__(self, name):
super(BoostComponentRequirement, self).__init__("boost-component")
self.name = name
class KF5ComponentRequirement(Requirement):
name: str
family = "kf5-component"
def __init__(self, name):
self.name = name
class GnulibDirectoryRequirement(Requirement):
directory: str
family = "gnulib"
def __init__(self, directory):
self.directory = directory

View file

@ -18,11 +18,8 @@
import logging
import subprocess
from typing import Optional, List, Type
from .. import UnidentifiedError, Requirement
from .. import UnidentifiedError
from ..fix_build import run_detecting_problems
from ..session import Session
class UnsatisfiedRequirements(Exception):
@ -31,22 +28,13 @@ class UnsatisfiedRequirements(Exception):
class Resolver(object):
name: str
def __init__(self, session, user_local):
raise NotImplementedError(self.__init__)
def install(self, requirements: List[Requirement]):
def install(self, requirements):
raise NotImplementedError(self.install)
def resolve(self, requirement: Requirement) -> Optional[Requirement]:
def resolve(self, requirement):
raise NotImplementedError(self.resolve)
def resolve_all(self, requirement: Requirement) -> List[Requirement]:
raise NotImplementedError(self.resolve_all)
def explain(self, requirements: List[Requirement]):
def explain(self, requirements):
raise NotImplementedError(self.explain)
def env(self):
@ -54,15 +42,13 @@ class Resolver(object):
class CPANResolver(Resolver):
name = "cpan"
def __init__(self, session, user_local=False, skip_tests=True):
self.session = session
self.user_local = user_local
self.skip_tests = skip_tests
def __str__(self):
return self.name
return "cpan"
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.session)
@ -123,8 +109,7 @@ class TlmgrResolver(Resolver):
self.repository = repository
def __str__(self):
if (self.repository.startswith('http://')
or self.repository.startswith('https://')):
if self.repository.startswith('http://') or self.repository.startswith('https://'):
return 'tlmgr(%r)' % self.repository
else:
return self.repository
@ -169,8 +154,7 @@ class TlmgrResolver(Resolver):
try:
run_detecting_problems(self.session, cmd, user=user)
except UnidentifiedError as e:
if ("tlmgr: user mode not initialized, "
"please read the documentation!") in e.lines:
if "tlmgr: user mode not initialized, please read the documentation!" in e.lines:
self.session.check_call(['tlmgr', 'init-usertree'])
else:
raise
@ -179,7 +163,6 @@ class TlmgrResolver(Resolver):
class CTANResolver(TlmgrResolver):
name = "ctan"
def __init__(self, session, user_local=False):
super(CTANResolver, self).__init__(
@ -187,16 +170,13 @@ class CTANResolver(TlmgrResolver):
class RResolver(Resolver):
name: str
def __init__(self, session, repos, user_local=False):
self.session = session
self.repos = repos
self.user_local = user_local
def __str__(self):
return self.name
return "cran"
def __repr__(self):
return "%s(%r, %r)" % (type(self).__name__, self.session, self.repos)
@ -241,14 +221,12 @@ class RResolver(Resolver):
class OctaveForgeResolver(Resolver):
name = "octave-forge"
def __init__(self, session, user_local=False):
self.session = session
self.user_local = user_local
def __str__(self):
return self.name
return "octave-forge"
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.session)
@ -289,8 +267,6 @@ class OctaveForgeResolver(Resolver):
class CRANResolver(RResolver):
name = "cran"
def __init__(self, session, user_local=False):
super(CRANResolver, self).__init__(
session, "http://cran.r-project.org", user_local=user_local
@ -298,25 +274,19 @@ class CRANResolver(RResolver):
class BioconductorResolver(RResolver):
name = "bioconductor"
def __init__(self, session, user_local=False):
super(BioconductorResolver, self).__init__(
session, "https://hedgehog.fhcrc.org/bioconductor",
user_local=user_local
session, "https://hedgehog.fhcrc.org/bioconductor", user_local=user_local
)
class HackageResolver(Resolver):
name = "hackage"
def __init__(self, session, user_local=False):
self.session = session
self.user_local = user_local
def __str__(self):
return self.name
return "hackage"
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.session)
@ -325,8 +295,7 @@ class HackageResolver(Resolver):
extra_args = []
if self.user_local:
extra_args.append("--user")
return (["cabal", "install"] + extra_args
+ [req.package for req in reqs])
return ["cabal", "install"] + extra_args + [req.package for req in reqs]
def install(self, requirements):
from ..requirements import HaskellPackageRequirement
@ -360,15 +329,12 @@ class HackageResolver(Resolver):
class PypiResolver(Resolver):
name = "pypi"
def __init__(self, session, user_local=False):
self.session = session
self.user_local = user_local
def __str__(self):
return self.name
return "pypi"
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.session)
@ -414,15 +380,12 @@ class PypiResolver(Resolver):
class GoResolver(Resolver):
name = "go"
def __init__(self, session, user_local):
self.session = session
self.user_local = user_local
def __str__(self):
return self.name
return "go"
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.session)
@ -463,26 +426,17 @@ NPM_COMMAND_PACKAGES = {
"del-cli": "del-cli",
"husky": "husky",
"cross-env": "cross-env",
"xo": "xo",
"standard": "standard",
"jshint": "jshint",
"if-node-version": "if-node-version",
"babel-cli": "babel",
"c8": "c8",
"prettier-standard": "prettier-standard",
}
class NpmResolver(Resolver):
name = "npm"
def __init__(self, session, user_local=False):
self.session = session
self.user_local = user_local
# TODO(jelmer): Handle user_local
def __str__(self):
return self.name
return "npm"
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.session)
@ -518,10 +472,7 @@ class NpmResolver(Resolver):
if not isinstance(requirement, NodePackageRequirement):
missing.append(requirement)
continue
cmd = ["npm", "install"]
if not self.user_local:
cmd.append('-g')
cmd.append(requirement.package)
cmd = ["npm", "-g", "install", requirement.package]
logging.info("npm: running %r", cmd)
run_detecting_problems(self.session, cmd, user=user)
if missing:
@ -578,7 +529,7 @@ class StackedResolver(Resolver):
raise UnsatisfiedRequirements(requirements)
NATIVE_RESOLVER_CLS: List[Type[Resolver]] = [
NATIVE_RESOLVER_CLS = [
CPANResolver,
CTANResolver,
PypiResolver,
@ -592,70 +543,24 @@ NATIVE_RESOLVER_CLS: List[Type[Resolver]] = [
def native_resolvers(session, user_local):
return StackedResolver(
[kls(session, user_local) for kls in NATIVE_RESOLVER_CLS])
return StackedResolver([kls(session, user_local) for kls in NATIVE_RESOLVER_CLS])
def select_resolvers(session, user_local, resolvers,
dep_server_url=None) -> Optional[Resolver]:
selected = []
for resolver in resolvers:
for kls in NATIVE_RESOLVER_CLS:
if kls.name == resolver:
selected.append(kls(session, user_local))
break
else:
if resolver == 'native':
selected.extend([
kls(session, user_local) for kls in NATIVE_RESOLVER_CLS])
elif resolver == 'apt':
if user_local:
raise NotImplementedError(
'user local not supported for apt')
if dep_server_url:
from .dep_server import DepServerAptResolver
selected.append(DepServerAptResolver.from_session(
session, dep_server_url))
else:
from .apt import AptResolver
selected.append(AptResolver.from_session(session))
else:
raise KeyError(resolver)
if len(selected) == 0:
return None
if len(selected) == 1:
return selected[0]
return StackedResolver(selected)
def auto_resolver(session: Session, explain: bool = False,
system_wide: Optional[bool] = None,
dep_server_url: Optional[str] = None):
def auto_resolver(session, explain=False):
# if session is SchrootSession or if we're root, use apt
from .apt import AptResolver
from ..session.schroot import SchrootSession
from ..session import get_user
user = get_user(session)
resolvers = []
if system_wide is None:
# TODO(jelmer): Check VIRTUAL_ENV, and prioritize PypiResolver if
# present?
if isinstance(session, SchrootSession) or user == "root" or explain:
system_wide = True
else:
system_wide = False
if system_wide:
try:
from .apt import AptResolver
except ModuleNotFoundError:
pass
else:
if dep_server_url:
from .dep_server import DepServerAptResolver
resolvers.append(
DepServerAptResolver.from_session(session, dep_server_url))
else:
resolvers.append(AptResolver.from_session(session))
resolvers.extend([kls(session, not system_wide)
for kls in NATIVE_RESOLVER_CLS])
# TODO(jelmer): Check VIRTUAL_ENV, and prioritize PypiResolver if
# present?
if isinstance(session, SchrootSession) or user == "root" or explain:
user_local = False
else:
user_local = True
if not user_local:
resolvers.append(AptResolver.from_session(session))
resolvers.extend([kls(session, user_local) for kls in NATIVE_RESOLVER_CLS])
return StackedResolver(resolvers)

File diff suppressed because it is too large Load diff

View file

@ -1,88 +0,0 @@
#!/usr/bin/python3
# Copyright (C) 2022 Jelmer Vernooij <jelmer@jelmer.uk>
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import asyncio
import logging
from typing import List
from aiohttp import (
ClientSession,
ClientConnectorError,
ClientResponseError,
ServerDisconnectedError,
)
from yarl import URL
from .. import Requirement, USER_AGENT
from ..debian.apt import AptManager
from .apt import AptRequirement, AptResolver
class DepServerError(Exception):
def __init__(self, inner):
self.inner = inner
async def resolve_apt_requirement_dep_server(
url: str, req: Requirement) -> List[AptRequirement]:
"""Resolve a requirement to an APT requirement with a dep server.
Args:
url: Dep server URL
req: Requirement to resolve
Returns:
List of Apt requirements.
"""
async with ClientSession() as session:
try:
async with session.post(URL(url) / "resolve-apt", headers={
'User-Agent': USER_AGENT},
json={'requirement': req.json()},
raise_for_status=True) as resp:
return [
AptRequirement._from_json(e) for e in await resp.json()]
except (ClientConnectorError, ClientResponseError,
ServerDisconnectedError) as e:
logging.warning('Unable to contact dep server: %r', e)
raise DepServerError(e)
class DepServerAptResolver(AptResolver):
def __init__(self, apt, dep_server_url, tie_breakers=None):
super(DepServerAptResolver, self).__init__(
apt, tie_breakers=tie_breakers)
self.dep_server_url = dep_server_url
@classmethod
def from_session(cls, session, dep_server_url, tie_breakers=None):
return cls(
AptManager.from_session(session), dep_server_url,
tie_breakers=tie_breakers)
def resolve_all(self, req: Requirement):
try:
req.json()
except NotImplementedError:
return super(DepServerAptResolver, self).resolve_all(req)
try:
return asyncio.run(
resolve_apt_requirement_dep_server(self.dep_server_url, req))
except DepServerError:
logging.warning('Falling back to resolving error locally')
return super(DepServerAptResolver, self).resolve_all(req)

View file

@ -69,14 +69,12 @@ class Session(object):
raise NotImplementedError(self.check_output)
def Popen(
self, argv, cwd: Optional[str] = None, user: Optional[str] = None,
**kwargs
self, argv, cwd: Optional[str] = None, user: Optional[str] = None, **kwargs
):
raise NotImplementedError(self.Popen)
def call(
self, argv: List[str], cwd: Optional[str] = None,
user: Optional[str] = None
self, argv: List[str], cwd: Optional[str] = None, user: Optional[str] = None
):
raise NotImplementedError(self.call)
@ -102,26 +100,17 @@ class Session(object):
def external_path(self, path: str) -> str:
raise NotImplementedError
def rmtree(self, path: str) -> str:
raise NotImplementedError
is_temporary: bool
class SessionSetupFailure(Exception):
"""Session failed to be set up."""
def __init__(self, reason, errlines=None):
self.reason = reason
self.errlines = errlines
def run_with_tee(session: Session,
args: List[str], **kwargs) -> Tuple[int, List[str]]:
def run_with_tee(session: Session, args: List[str], **kwargs):
if "stdin" not in kwargs:
kwargs["stdin"] = subprocess.DEVNULL
p = session.Popen(
args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs)
p = session.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs)
contents = []
while p.poll() is None:
line = p.stdout.readline()
@ -132,8 +121,7 @@ def run_with_tee(session: Session,
def get_user(session):
return session.check_output(
["sh", "-c", "echo $USER"], cwd="/").decode().strip()
return session.check_output(["echo", "$USER"], cwd="/").decode().strip()
def which(session, name):

View file

@ -20,7 +20,6 @@ from . import Session, NoSessionOpen, SessionAlreadyOpen
import contextlib
import os
import shutil
import subprocess
import tempfile
from typing import Optional, Dict, List
@ -73,8 +72,7 @@ class PlainSession(Session):
close_fds: bool = True,
):
argv = self._prepend_user(user, argv)
return subprocess.check_call(
argv, cwd=cwd, env=env, close_fds=close_fds)
return subprocess.check_call(argv, cwd=cwd, env=env, close_fds=close_fds)
def check_output(
self,
@ -86,19 +84,13 @@ class PlainSession(Session):
argv = self._prepend_user(user, argv)
return subprocess.check_output(argv, cwd=cwd, env=env)
def Popen(
self, args, stdout=None, stderr=None, stdin=None, user=None,
cwd=None, env=None):
def Popen(self, args, stdout=None, stderr=None, stdin=None, user=None, cwd=None, env=None):
args = self._prepend_user(user, args)
return subprocess.Popen(
args, stdout=stdout, stderr=stderr, stdin=stdin, cwd=cwd, env=env)
return subprocess.Popen(args, stdout=stdout, stderr=stderr, stdin=stdin, cwd=cwd, env=env)
def exists(self, path):
return os.path.exists(path)
def rmtree(self, path):
return shutil.rmtree(path)
def scandir(self, path):
return os.scandir(path)

View file

@ -66,38 +66,25 @@ class SchrootSession(Session):
if line.startswith(b"E: "):
logging.error("%s", line[3:].decode(errors="replace"))
logging.warning(
"Failed to close schroot session %s, leaving stray.",
self.session_id
"Failed to close schroot session %s, leaving stray.", self.session_id
)
self.session_id = None
return False
self.session_id = None
self._location = None
return True
def __enter__(self) -> "Session":
if self.session_id is not None:
raise SessionAlreadyOpen(self)
stderr = tempfile.TemporaryFile()
try:
self.session_id = (
subprocess.check_output(
["schroot", "-c", self.chroot, "-b"], stderr=stderr)
subprocess.check_output(["schroot", "-c", self.chroot, "-b"])
.strip()
.decode()
)
except subprocess.CalledProcessError:
stderr.seek(0)
errlines = stderr.readlines()
if len(errlines) == 1:
raise SessionSetupFailure(
errlines[0].rstrip().decode(), errlines=errlines)
elif len(errlines) == 0:
raise SessionSetupFailure(
"No output from schroot", errlines=errlines)
else:
raise SessionSetupFailure(
errlines[-1].decode(), errlines=errlines)
# TODO(jelmer): Capture stderr and forward in SessionSetupFailure
raise SessionSetupFailure()
logging.info(
"Opened schroot session %s (from %s)", self.session_id, self.chroot
)
@ -169,28 +156,24 @@ class SchrootSession(Session):
env: Optional[Dict[str, str]] = None,
) -> bytes:
try:
return subprocess.check_output(
self._run_argv(argv, cwd, user, env=env))
return subprocess.check_output(self._run_argv(argv, cwd, user, env=env))
except subprocess.CalledProcessError as e:
raise subprocess.CalledProcessError(e.returncode, argv)
def Popen(
self, argv, cwd: Optional[str] = None, user: Optional[str] = None,
**kwargs
self, argv, cwd: Optional[str] = None, user: Optional[str] = None, **kwargs
):
return subprocess.Popen(self._run_argv(argv, cwd, user), **kwargs)
def call(
self, argv: List[str], cwd: Optional[str] = None,
user: Optional[str] = None
self, argv: List[str], cwd: Optional[str] = None, user: Optional[str] = None
):
return subprocess.call(self._run_argv(argv, cwd, user))
def create_home(self) -> None:
"""Create the user's home directory."""
home = (
self.check_output(
["sh", "-c", "echo $HOME"], cwd="/").decode().rstrip("\n")
self.check_output(["sh", "-c", "echo $HOME"], cwd="/").decode().rstrip("\n")
)
user = (
self.check_output(["sh", "-c", "echo $LOGNAME"], cwd="/")
@ -206,8 +189,7 @@ class SchrootSession(Session):
return os.path.join(self.location, path.lstrip("/"))
if self._cwd is None:
raise ValueError("no cwd set")
return os.path.join(
self.location, os.path.join(self._cwd, path).lstrip("/"))
return os.path.join(self.location, os.path.join(self._cwd, path).lstrip("/"))
def exists(self, path: str) -> bool:
fullpath = self.external_path(path)
@ -221,17 +203,13 @@ class SchrootSession(Session):
fullpath = self.external_path(path)
return os.mkdir(fullpath)
def rmtree(self, path: str):
import shutil
fullpath = self.external_path(path)
return shutil.rmtree(fullpath)
def setup_from_vcs(
self, tree, include_controldir: Optional[bool] = None, subdir="package"
):
from ..vcs import dupe_vcs_tree, export_vcs_tree
build_dir = os.path.join(self.location, "build")
directory = tempfile.mkdtemp(dir=build_dir)
reldir = "/" + os.path.relpath(directory, self.location)
@ -250,7 +228,7 @@ class SchrootSession(Session):
directory = tempfile.mkdtemp(dir=build_dir)
reldir = "/" + os.path.relpath(directory, self.location)
export_directory = os.path.join(directory, subdir)
shutil.copytree(path, export_directory, symlinks=True)
shutil.copytree(path, export_directory, dirs_exist_ok=True)
return export_directory, os.path.join(reldir, subdir)
is_temporary = True

View file

@ -15,25 +15,16 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from functools import partial
from .buildsystem import NoBuildToolsFound
from .fix_build import iterate_with_build_fixers
from .logs import NoLogManager
def run_test(session, buildsystems, resolver, fixers, log_manager=None):
def run_test(session, buildsystems, resolver, fixers):
# Some things want to write to the user's home directory,
# e.g. pip caches in ~/.cache
session.create_home()
if log_manager is None:
log_manager = NoLogManager()
for buildsystem in buildsystems:
iterate_with_build_fixers(
fixers, log_manager.wrap(
partial(buildsystem.test, session, resolver)))
buildsystem.test(session, resolver, fixers)
return
raise NoBuildToolsFound()

View file

@ -23,13 +23,10 @@ import unittest
def test_suite():
names = [
'buildlog',
'logs',
"debian_build",
]
if os.path.exists("/usr/bin/dpkg-architecture"):
names.append("debian_build")
names.append("debian_fix_build")
names.append("resolver_apt")
module_names = ["tests.test_" + name for name in names]
module_names = ["ognibuild.tests.test_" + name for name in names]
loader = unittest.TestLoader()
return loader.loadTestsFromNames(module_names)

View file

@ -17,17 +17,8 @@
import datetime
import os
import sys
from debian.changelog import Version
from ognibuild.debian.build import (
add_dummy_changelog_entry,
get_build_architecture,
version_add_suffix,
_builddeb_command,
DEFAULT_BUILDER,
)
from ..debian.build import add_dummy_changelog_entry, get_build_architecture
from breezy.tests import TestCaseWithTransport, TestCase
@ -159,43 +150,3 @@ class BuildArchitectureTests(TestCase):
def test_is_str(self):
self.assertIsInstance(get_build_architecture(), str)
class VersionAddSuffixTests(TestCase):
def test_native(self):
self.assertEqual(
Version('1.0~jan+lint4'),
version_add_suffix(Version('1.0~jan+lint3'), '~jan+lint'))
self.assertEqual(
Version('1.0~jan+lint1'),
version_add_suffix(Version('1.0'), '~jan+lint'))
def test_normal(self):
self.assertEqual(
Version('1.0-1~jan+lint4'),
version_add_suffix(Version('1.0-1~jan+lint3'), '~jan+lint'))
self.assertEqual(
Version('1.0-1~jan+lint1'),
version_add_suffix(Version('1.0-1'), '~jan+lint'))
self.assertEqual(
Version('0.0.12-1~jan+lint1'),
version_add_suffix(Version('0.0.12-1'), '~jan+lint'))
self.assertEqual(
Version('0.0.12-1~jan+unchanged1~jan+lint1'),
version_add_suffix(
Version('0.0.12-1~jan+unchanged1'), '~jan+lint'))
class BuilddebCommandTests(TestCase):
def test_simple(self):
self.assertEqual(
[sys.executable, "-m", "breezy", "builddeb",
"--guess-upstream-branch-url", "--builder=" + DEFAULT_BUILDER],
_builddeb_command())
self.assertEqual(
[sys.executable, "-m", "breezy", "builddeb",
"--guess-upstream-branch-url", "--builder=" + DEFAULT_BUILDER,
"--result-dir=/tmp/blah"],
_builddeb_command(result_dir="/tmp/blah"))

View file

@ -29,15 +29,13 @@ from buildlog_consultant.common import (
MissingRubyGem,
MissingValaPackage,
)
from ognibuild.debian.apt import AptManager, FileSearcher
from ognibuild.debian.fix_build import (
from ..debian.apt import AptManager, FileSearcher
from ..debian.fix_build import (
resolve_error,
versioned_package_fixers,
apt_fixers,
DebianPackagingContext,
add_build_dependency,
)
from ognibuild.resolver.apt import AptRequirement
from breezy.commit import NullCommitReporter
from breezy.tests import TestCaseWithTransport
@ -46,7 +44,7 @@ class DummyAptSearcher(FileSearcher):
def __init__(self, files):
self._apt_files = files
async def search_files(self, path, regex=False, case_insensitive=False):
def search_files(self, path, regex=False, case_insensitive=False):
for p, pkg in sorted(self._apt_files.items()):
if case_insensitive:
flags = re.I
@ -99,7 +97,7 @@ blah (0.1) UNRELEASED; urgency=medium
self._apt_files = {}
def resolve(self, error, context=("build",)):
from ognibuild.session.plain import PlainSession
from ..session.plain import PlainSession
session = PlainSession()
apt = AptManager(session)
@ -111,8 +109,7 @@ blah (0.1) UNRELEASED; urgency=medium
update_changelog=True,
commit_reporter=NullCommitReporter(),
)
fixers = versioned_package_fixers(
session, context, apt) + apt_fixers(apt, context)
fixers = versioned_package_fixers(session, context, apt) + apt_fixers(apt, context)
return resolve_error(error, ("build",), fixers)
def get_build_deps(self):
@ -121,8 +118,7 @@ blah (0.1) UNRELEASED; urgency=medium
def test_missing_command_unknown(self):
self._apt_files = {}
self.assertFalse(self.resolve(
MissingCommand("acommandthatdoesnotexist")))
self.assertFalse(self.resolve(MissingCommand("acommandthatdoesnotexist")))
def test_missing_command_brz(self):
self._apt_files = {
@ -134,8 +130,7 @@ blah (0.1) UNRELEASED; urgency=medium
self.overrideEnv("DEBFULLNAME", "Jelmer Vernooij")
self.assertTrue(self.resolve(MissingCommand("brz")))
self.assertEqual("libc6, brz", self.get_build_deps())
rev = self.tree.branch.repository.get_revision(
self.tree.branch.last_revision())
rev = self.tree.branch.repository.get_revision(self.tree.branch.last_revision())
self.assertEqual("Add missing build dependency on brz.\n", rev.message)
self.assertFalse(self.resolve(MissingCommand("brz")))
self.assertEqual("libc6, brz", self.get_build_deps())
@ -158,12 +153,10 @@ blah (0.1) UNRELEASED; urgency=medium
def test_missing_ruby_file_from_gem(self):
self._apt_files = {
"/usr/share/rubygems-integration/all/gems/activesupport-"
"5.2.3/lib/active_support/core_ext/string/strip.rb":
"ruby-activesupport"
"5.2.3/lib/active_support/core_ext/string/strip.rb": "ruby-activesupport"
}
self.assertTrue(
self.resolve(MissingRubyFile(
"active_support/core_ext/string/strip"))
self.resolve(MissingRubyFile("active_support/core_ext/string/strip"))
)
self.assertEqual("libc6, ruby-activesupport", self.get_build_deps())
@ -180,8 +173,7 @@ blah (0.1) UNRELEASED; urgency=medium
self.assertEqual("libc6, ruby-bio (>= 2.0.3)", self.get_build_deps())
def test_missing_perl_module(self):
self._apt_files = {
"/usr/share/perl5/App/cpanminus/fatscript.pm": "cpanminus"}
self._apt_files = {"/usr/share/perl5/App/cpanminus/fatscript.pm": "cpanminus"}
self.assertTrue(
self.resolve(
MissingPerlModule(
@ -208,34 +200,28 @@ blah (0.1) UNRELEASED; urgency=medium
def test_missing_pkg_config(self):
self._apt_files = {
"/usr/lib/x86_64-linux-gnu/pkgconfig/xcb-xfixes.pc":
"libxcb-xfixes0-dev"
"/usr/lib/x86_64-linux-gnu/pkgconfig/xcb-xfixes.pc": "libxcb-xfixes0-dev"
}
self.assertTrue(self.resolve(MissingPkgConfig("xcb-xfixes")))
self.assertEqual("libc6, libxcb-xfixes0-dev", self.get_build_deps())
def test_missing_pkg_config_versioned(self):
self._apt_files = {
"/usr/lib/x86_64-linux-gnu/pkgconfig/xcb-xfixes.pc":
"libxcb-xfixes0-dev"
"/usr/lib/x86_64-linux-gnu/pkgconfig/xcb-xfixes.pc": "libxcb-xfixes0-dev"
}
self.assertTrue(self.resolve(MissingPkgConfig("xcb-xfixes", "1.0")))
self.assertEqual(
"libc6, libxcb-xfixes0-dev (>= 1.0)", self.get_build_deps())
self.assertEqual("libc6, libxcb-xfixes0-dev (>= 1.0)", self.get_build_deps())
def test_missing_python_module(self):
self._apt_files = {
"/usr/lib/python3/dist-packages/m2r.py": "python3-m2r"}
self._apt_files = {"/usr/lib/python3/dist-packages/m2r.py": "python3-m2r"}
self.assertTrue(self.resolve(MissingPythonModule("m2r")))
self.assertEqual("libc6, python3-m2r", self.get_build_deps())
def test_missing_go_package(self):
self._apt_files = {
"/usr/share/gocode/src/github.com/chzyer/readline/utils_test.go":
"golang-github-chzyer-readline-dev",
"/usr/share/gocode/src/github.com/chzyer/readline/utils_test.go": "golang-github-chzyer-readline-dev",
}
self.assertTrue(self.resolve(
MissingGoPackage("github.com/chzyer/readline")))
self.assertTrue(self.resolve(MissingGoPackage("github.com/chzyer/readline")))
self.assertEqual(
"libc6, golang-github-chzyer-readline-dev", self.get_build_deps()
)
@ -246,63 +232,3 @@ blah (0.1) UNRELEASED; urgency=medium
}
self.assertTrue(self.resolve(MissingValaPackage("posix")))
self.assertEqual("libc6, valac-0.48-vapi", self.get_build_deps())
class AddBuildDependencyTests(TestCaseWithTransport):
def setUp(self):
super(AddBuildDependencyTests, self).setUp()
self.tree = self.make_branch_and_tree(".")
self.build_tree_contents(
[
("debian/",),
(
"debian/control",
"""\
Source: blah
Build-Depends: libc6
Package: python-blah
Depends: ${python3:Depends}
Description: A python package
Foo
""",
),
(
"debian/changelog",
"""\
blah (0.1) UNRELEASED; urgency=medium
* Initial release. (Closes: #XXXXXX)
-- Jelmer Vernooij <jelmer@debian.org> Sat, 04 Apr 2020 14:12:13 +0000
""",
),
]
)
self.tree.add(["debian", "debian/control", "debian/changelog"])
self.tree.commit("Initial commit")
self.context = DebianPackagingContext(
self.tree,
subpath="",
committer="ognibuild <ognibuild@jelmer.uk>",
update_changelog=True,
commit_reporter=NullCommitReporter(),
)
def test_already_present(self):
requirement = AptRequirement.simple('libc6')
self.assertFalse(add_build_dependency(self.context, requirement))
def test_basic(self):
requirement = AptRequirement.simple('foo')
self.assertTrue(add_build_dependency(self.context, requirement))
self.assertFileEqual("""\
Source: blah
Build-Depends: libc6, foo
Package: python-blah
Depends: ${python3:Depends}
Description: A python package
Foo
""", 'debian/control')

View file

@ -1,253 +0,0 @@
#!/usr/bin/python3
# Copyright (C) 2020-2021 Jelmer Vernooij <jelmer@jelmer.uk>
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from dataclasses import dataclass, field
from typing import Optional, Dict, Any
from debian.changelog import Version
import logging
import re
from . import Requirement
from .requirements import (
CargoCrateRequirement,
GoPackageRequirement,
PythonPackageRequirement,
)
from .resolver.apt import AptRequirement, OneOfRequirement
@dataclass
class UpstreamInfo:
name: Optional[str]
buildsystem: Optional[str] = None
branch_url: Optional[str] = None
branch_subpath: Optional[str] = None
tarball_url: Optional[str] = None
version: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)
def json(self):
return {
'name': self.name,
'buildsystem': self.buildsystem,
'branch_url': self.branch_url,
'branch_subpath': self.branch_subpath,
'tarball_url': self.tarball_url,
'version': self.version
}
def go_base_name(package):
(hostname, path) = package.split('/', 1)
if hostname == "github.com":
hostname = "github"
if hostname == "gopkg.in":
hostname = "gopkg"
path = path.rstrip('/').replace("/", "-")
if path.endswith('.git'):
path = path[:-4]
return (hostname + path).replace("_", "-").lower()
def load_crate_info(crate):
import urllib.error
from urllib.request import urlopen, Request
import json
http_url = 'https://crates.io/api/v1/crates/%s' % crate
headers = {'User-Agent': 'debianize', 'Accept': 'application/json'}
http_contents = urlopen(Request(http_url, headers=headers)).read()
try:
return json.loads(http_contents)
except urllib.error.HTTPError as e:
if e.code == 404:
logging.warning('No crate %r', crate)
return None
raise
def find_python_package_upstream(requirement):
import urllib.error
from urllib.request import urlopen, Request
import json
http_url = 'https://pypi.org/pypi/%s/json' % requirement.package
headers = {'User-Agent': 'ognibuild', 'Accept': 'application/json'}
try:
http_contents = urlopen(
Request(http_url, headers=headers)).read()
except urllib.error.HTTPError as e:
if e.code == 404:
logging.warning('No pypi project %r', requirement.package)
return None
raise
pypi_data = json.loads(http_contents)
upstream_branch = None
for name, url in pypi_data['info']['project_urls'].items():
if name.lower() in ('github', 'repository'):
upstream_branch = url
tarball_url = None
for url_data in pypi_data['urls']:
if url_data.get('package_type') == 'sdist':
tarball_url = url_data['url']
return UpstreamInfo(
branch_url=upstream_branch, branch_subpath='',
name='python-%s' % pypi_data['info']['name'],
tarball_url=tarball_url)
def find_go_package_upstream(requirement):
if requirement.package.startswith('github.com/'):
return UpstreamInfo(
name='golang-%s' % go_base_name(requirement.package),
branch_url='https://%s' % '/'.join(
requirement.package.split('/')[:3]),
branch_subpath='')
def find_cargo_crate_upstream(requirement):
import semver
from debmutate.debcargo import semver_pair
data = load_crate_info(requirement.crate)
if data is None:
return None
upstream_branch = data['crate']['repository']
name = 'rust-' + data['crate']['name'].replace('_', '-')
version = None
if requirement.api_version is not None:
for version_info in data['versions']:
if (not version_info['num'].startswith(
requirement.api_version + '.')
and not version_info['num'] == requirement.api_version):
continue
if version is None:
version = semver.VersionInfo.parse(version_info['num'])
else:
version = semver.max_ver(
version, semver.VersionInfo.parse(version_info['num']))
if version is None:
logging.warning(
'Unable to find version of crate %s '
'that matches API version %s',
name, requirement.api_version)
else:
name += '-' + semver_pair(str(version))
return UpstreamInfo(
branch_url=upstream_branch, branch_subpath=None,
name=name, version=str(version) if version else None,
metadata={'X-Cargo-Crate': data['crate']['name']},
buildsystem='cargo')
def apt_to_cargo_requirement(m, rels):
name = m.group(1)
api_version = m.group(2)
if m.group(3):
features = set(m.group(3)[1:].split('-'))
else:
features = set()
if not rels:
minimum_version = None
elif len(rels) == 1 and rels[0][0] == '>=':
minimum_version = Version(rels[0][1]).upstream_version
else:
logging.warning('Unable to parse Debian version %r', rels)
minimum_version = None
return CargoCrateRequirement(
name, api_version=api_version,
features=features, minimum_version=minimum_version)
def apt_to_python_requirement(m, rels):
name = m.group(2)
python_version = m.group(1)
if not rels:
minimum_version = None
elif len(rels) == 1 and rels[0][0] == '>=':
minimum_version = Version(rels[0][1]).upstream_version
else:
logging.warning('Unable to parse Debian version %r', rels)
minimum_version = None
return PythonPackageRequirement(
name, python_version=(python_version or None),
minimum_version=minimum_version)
def apt_to_go_requirement(m, rels):
parts = m.group(1).split('-')
if parts[0] == 'github':
parts[0] = 'github.com'
if parts[0] == 'gopkg':
parts[0] = 'gopkg.in'
if not rels:
version = None
elif len(rels) == 1 and rels[0][0] == '=':
version = Version(rels[0][1]).upstream_version
else:
logging.warning('Unable to parse Debian version %r', rels)
version = None
return GoPackageRequirement('/'.join(parts), version=version)
BINARY_PACKAGE_UPSTREAM_MATCHERS = [
(r'librust-(.*)-([^-+]+)(\+.*?)-dev', apt_to_cargo_requirement),
(r'python([0-9.]*)-(.*)', apt_to_python_requirement),
(r'golang-(.*)-dev', apt_to_go_requirement),
]
_BINARY_PACKAGE_UPSTREAM_MATCHERS = [
(re.compile(r), fn) for (r, fn) in BINARY_PACKAGE_UPSTREAM_MATCHERS]
def find_apt_upstream(requirement: AptRequirement) -> Optional[UpstreamInfo]:
for option in requirement.relations:
for rel in option:
for matcher, fn in _BINARY_PACKAGE_UPSTREAM_MATCHERS:
m = matcher.fullmatch(rel['name'])
if m:
upstream_requirement = fn(
m, [rel['version']] if rel['version'] else [])
return find_upstream(upstream_requirement)
logging.warning(
'Unable to map binary package name %s to upstream',
rel['name'])
return None
def find_or_upstream(requirement: OneOfRequirement) -> Optional[UpstreamInfo]:
for req in requirement.elements:
info = find_upstream(req)
if info is not None:
return info
return None
UPSTREAM_FINDER = {
'python-package': find_python_package_upstream,
'go-package': find_go_package_upstream,
'cargo-crate': find_cargo_crate_upstream,
'apt': find_apt_upstream,
'or': find_or_upstream,
}
def find_upstream(requirement: Requirement) -> Optional[UpstreamInfo]:
try:
return UPSTREAM_FINDER[requirement.family](requirement)
except KeyError:
return None

View file

@ -43,8 +43,7 @@ def dupe_vcs_tree(tree, directory):
tree = tree.basis_tree()
try:
result = tree._repository.controldir.sprout(
directory, create_tree_if_local=True,
revision_id=tree.get_revision_id()
directory, create_tree_if_local=True, revision_id=tree.get_revision_id()
)
except OSError as e:
if e.errno == errno.ENOSPC:

View file

@ -1,3 +0,0 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

14
releaser.conf Normal file
View file

@ -0,0 +1,14 @@
name: "ognibuild"
timeout_days: 5
tag_name: "v$VERSION"
verify_command: "python3 setup.py test"
update_version {
path: "setup.py"
match: "^ version=\"(.*)\",$"
new_line: " version=\"$VERSION\","
}
update_version {
path: "ognibuild/__init__.py"
match: "^__version__ = \\((.*)\\)$"
new_line: "__version__ = $TUPLED_VERSION"
}

View file

@ -1,89 +0,0 @@
#!/usr/bin/python3
import argparse
from contextlib import ExitStack
import logging
import sys
from typing import Dict, List
from ognibuild.buildsystem import NoBuildToolsFound, detect_buildsystems
from ognibuild.requirements import Requirement
from ognibuild.resolver.apt import AptResolver
from ognibuild.session.plain import PlainSession
parser = argparse.ArgumentParser('report-apt-deps-status')
parser.add_argument('directory', type=str, default='.', nargs='?')
parser.add_argument(
'--detailed', action='store_true', help='Show detailed analysis')
args = parser.parse_args()
logging.basicConfig(format='%(message)s', level=logging.INFO)
session = PlainSession()
with ExitStack() as es:
es.enter_context(session)
session.chdir(args.directory)
resolver = AptResolver.from_session(session)
try:
bss = list(detect_buildsystems(args.directory))
except NoBuildToolsFound:
logging.fatal('No build tools found')
sys.exit(1)
logging.debug("Detected buildsystems: %s", ", ".join(map(str, bss)))
deps: Dict[str, List[Requirement]] = {}
for buildsystem in bss:
try:
declared_reqs = buildsystem.get_declared_dependencies(session, [])
for stage, req in declared_reqs:
deps.setdefault(stage, []).append(req)
except NotImplementedError:
logging.warning(
'Unable to get dependencies from buildsystem %r, skipping',
buildsystem)
continue
if args.detailed:
for stage, reqs in deps.items():
logging.info("Stage: %s", stage)
for req in reqs:
apt_req = resolver.resolve(req)
logging.info("%s: %s", req, apt_req.pkg_relation_str())
logging.info('')
else:
build_depends = []
test_depends = []
run_depends = []
unresolved = []
for stage, reqs in deps.items():
for req in reqs:
apt_req = resolver.resolve(req)
if apt_req is None:
unresolved.append(req)
elif stage == 'core':
build_depends.append(apt_req)
run_depends.append(apt_req)
elif stage == 'build':
build_depends.append(apt_req)
elif stage == 'test':
test_depends.append(apt_req)
else:
raise NotImplementedError('stage %s not supported' % stage)
if build_depends:
logging.info(
'Build-Depends: %s',
', '.join([d.pkg_relation_str() for d in build_depends]))
if test_depends:
logging.info(
'Test-Depends: %s',
', '.join([d.pkg_relation_str() for d in test_depends]))
if run_depends:
logging.info(
'Depends: %s',
', '.join([d.pkg_relation_str() for d in run_depends]))
if unresolved:
sys.stdout.write('\n')
logging.warning(
'Unable to find apt packages for the following dependencies:')
for req in unresolved:
logging.warning('* %s', req)

View file

@ -1,65 +1,13 @@
[metadata]
name = ognibuild
description = Detect and run any build system
version = attr:ognibuild.__version__
maintainer = Jelmer Vernooij
maintainer_email = jelmer@jelmer.uk
license = GNU GPLv2 or later
url = https://jelmer.uk/code/ognibuild
classifiers =
Development Status :: 4 - Beta
License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: Implementation :: CPython
Operating System :: POSIX
[options]
packages =
ognibuild
ognibuild.debian
ognibuild.resolver
ognibuild.session
scripts = scripts/report-apt-deps-status
install_requires =
breezy>=3.2
buildlog-consultant>=0.0.21
requirements-parser
toml
setuptools
ruamel.yaml
tests_require =
testtools
types-toml
[options.entry_points]
console_scripts =
ogni=ognibuild.__main__:main
deb-fix-build=ognibuild.debian.fix_build:main
[options.extras_require]
dev =
testtools
debian =
debmutate
python_debian
python_apt
brz-debian
lz4
remote =
breezy
dulwich
dep_server =
aiohttp
aiohttp-openmetrics
gcp = google-cloud-logging
[flake8]
banned-modules = silver-platter = Should not use silver-platter
exclude = build,.eggs/
[mypy]
ignore_missing_imports = True
[bdist_wheel]
universal = 1
[egg_info]
tag_build =
tag_date = 0

View file

@ -1,3 +1,40 @@
#!/usr/bin/python3
#!/usr/bin/env python3
# encoding: utf-8
from setuptools import setup
setup()
setup(name="ognibuild",
description="Detect and run any build system",
version="0.0.7",
maintainer="Jelmer Vernooij",
maintainer_email="jelmer@jelmer.uk",
license="GNU GPLv2 or later",
url="https://jelmer.uk/code/ognibuild",
packages=['ognibuild', 'ognibuild.tests', 'ognibuild.debian', 'ognibuild.resolver', 'ognibuild.session'],
classifiers=[
'Development Status :: 4 - Beta',
'License :: OSI Approved :: '
'GNU General Public License v2 or later (GPLv2+)',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: Implementation :: CPython',
'Operating System :: POSIX',
],
entry_points={
"console_scripts": [
"ogni=ognibuild.__main__:main",
"deb-fix-build=ognibuild.debian.fix_build:main",
]
},
install_requires=[
'breezy',
'buildlog-consultant>=0.0.10',
'requirements-parser',
],
extras_require={
'debian': ['debmutate', 'python_debian', 'python_apt'],
},
tests_require=['python_debian', 'buildlog-consultant', 'breezy', 'testtools'],
test_suite='ognibuild.tests.test_suite',
)

View file

@ -1,47 +0,0 @@
#!/usr/bin/python
# Copyright (C) 2022 Jelmer Vernooij <jelmer@jelmer.uk>
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from ognibuild.buildlog import PROBLEM_CONVERTERS
from buildlog_consultant import (
problem_clses,
__version__ as buildlog_consultant_version,
)
from unittest import TestCase
class TestProblemsExists(TestCase):
def test_exist(self):
for entry in PROBLEM_CONVERTERS:
if len(entry) == 2:
problem_kind, fn = entry
min_version = None
elif len(entry) == 3:
problem_kind, fn, min_version = entry
else:
raise TypeError(entry)
if min_version is not None:
min_version_tuple = tuple(
[int(x) for x in min_version.split('.')])
if buildlog_consultant_version < min_version_tuple:
continue
self.assertTrue(
problem_kind in problem_clses,
f"{problem_kind} does not exist in known "
"buildlog-consultant problem kinds")

View file

@ -1,95 +0,0 @@
#!/usr/bin/python
# Copyright (C) 2022 Jelmer Vernooij <jelmer@jelmer.uk>
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import os
import sys
import tempfile
from unittest import TestCase
from ognibuild.logs import (
copy_output,
redirect_output,
rotate_logfile,
DirectoryLogManager,
)
class TestCopyOutput(TestCase):
def test_no_tee(self):
with tempfile.TemporaryDirectory() as td:
p = os.path.join(td, 'foo.log')
with copy_output(p, tee=False):
sys.stdout.write('lala\n')
sys.stdout.flush()
with open(p, 'r') as f:
self.assertEqual('lala\n', f.read())
def test_tee(self):
with tempfile.TemporaryDirectory() as td:
p = os.path.join(td, 'foo.log')
with copy_output(p, tee=True):
sys.stdout.write('lala\n')
sys.stdout.flush()
with open(p, 'r') as f:
self.assertEqual('lala\n', f.read())
class TestRedirectOutput(TestCase):
def test_simple(self):
with tempfile.TemporaryDirectory() as td:
p = os.path.join(td, 'foo.log')
with open(p, 'w') as f:
with redirect_output(f):
sys.stdout.write('lala\n')
sys.stdout.flush()
with open(p, 'r') as f:
self.assertEqual('lala\n', f.read())
class TestRotateLogfile(TestCase):
def test_does_not_exist(self):
with tempfile.TemporaryDirectory() as td:
p = os.path.join(td, 'foo.log')
rotate_logfile(p)
self.assertEqual([], os.listdir(td))
def test_simple(self):
with tempfile.TemporaryDirectory() as td:
p = os.path.join(td, 'foo.log')
with open(p, 'w') as f:
f.write('contents\n')
rotate_logfile(p)
self.assertEqual(['foo.log.1'], os.listdir(td))
class TestLogManager(TestCase):
def test_simple(self):
with tempfile.TemporaryDirectory() as td:
p = os.path.join(td, 'foo.log')
lm = DirectoryLogManager(p, mode='redirect')
def writesomething():
sys.stdout.write('foo\n')
sys.stdout.flush()
fn = lm.wrap(writesomething)
fn()
with open(p, 'r') as f:
self.assertEqual('foo\n', f.read())

View file

@ -1,47 +0,0 @@
#!/usr/bin/python
# Copyright (C) 2022 Jelmer Vernooij <jelmer@jelmer.uk>
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from unittest import TestCase
from ognibuild.resolver.apt import get_possible_python3_paths_for_python_object
class TestPython3Paths(TestCase):
def test_paths(self):
self.assertEqual([
'/usr/lib/python3/dist\\-packages/dulwich/__init__\\.py',
'/usr/lib/python3/dist\\-packages/dulwich\\.py',
'/usr/lib/python3\\.[0-9]+/'
'lib\\-dynload/dulwich.cpython\\-.*\\.so',
'/usr/lib/python3\\.[0-9]+/dulwich\\.py',
'/usr/lib/python3\\.[0-9]+/dulwich/__init__\\.py'],
get_possible_python3_paths_for_python_object('dulwich'))
self.assertEqual([
'/usr/lib/python3/dist\\-packages/cleo/foo/__init__\\.py',
'/usr/lib/python3/dist\\-packages/cleo/foo\\.py',
'/usr/lib/python3\\.[0-9]+/'
'lib\\-dynload/cleo/foo.cpython\\-.*\\.so',
'/usr/lib/python3\\.[0-9]+/cleo/foo\\.py',
'/usr/lib/python3\\.[0-9]+/cleo/foo/__init__\\.py',
'/usr/lib/python3/dist\\-packages/cleo/__init__\\.py',
'/usr/lib/python3/dist\\-packages/cleo\\.py',
'/usr/lib/python3\\.[0-9]+/lib\\-dynload/cleo.cpython\\-.*\\.so',
'/usr/lib/python3\\.[0-9]+/cleo\\.py',
'/usr/lib/python3\\.[0-9]+/cleo/__init__\\.py'],
get_possible_python3_paths_for_python_object('cleo.foo'))