From 6e73b9adcb004f83a4c2f364ef89a99bf7b53a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:00:40 +0000 Subject: [PATCH 01/40] Create debian/ directory --- debian/changelog | 5 +++++ debian/control | 7 +++++++ debian/rules | 4 ++++ 3 files changed, 16 insertions(+) create mode 100644 debian/changelog create mode 100644 debian/control create mode 100755 debian/rules diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..c039f17 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +python-ognibuild (0.0.1~git20201031.4cbc8df-1) UNRELEASED; urgency=low + + * Initial release. + + -- Jelmer Vernooij Fri, 05 Feb 2021 03:00:40 +0000 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..2afb26e --- /dev/null +++ b/debian/control @@ -0,0 +1,7 @@ +Rules-Requires-Root: no +Standards-Version: 4.5.1 +Build-Depends: debhelper-compat (= 12), dh-sequence-python3 +Source: python-ognibuild + +Package: python3-python-ognibuild +Architecture: all diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..11cd9df --- /dev/null +++ b/debian/rules @@ -0,0 +1,4 @@ +#!/usr/bin/make -f + +%: + dh $@ --buildsystem=pybuild From 4a8ee2fb2b45e5161ab4c731845b007d37b2d4c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:00:48 +0000 Subject: [PATCH 02/40] Create a debian/copyright file. Changes-By: lintian-brush Fixes: lintian: no-copyright-file See-also: https://lintian.debian.org/tags/no-copyright-file.html --- debian/copyright | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 debian/copyright diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..5e60a61 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,21 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + +Files: * +Copyright: Unknown +License: GPL-2+ +Comment: No explicit license found, using license(s) from: + LICENSE + +Files: LICENSE build/* dist/ognibuild-0.0.1.tar.gz ognibuild/* +Copyright: 1989-1991, Free Software Foundation, Inc + 2019-2020, Jelmer Vernooij +License: GPL-2+ + +Files: debian/* +Copyright: Unknown +License: GPL-2+ +Comment: No explicit license found, using license(s) from: + LICENSE + +License: GPL-2+ +Comment: Add the corresponding license text here From 2cab6f79f18f45a412ca68267180eb2325f54c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:00:48 +0000 Subject: [PATCH 03/40] Add description for binary packages: python3-python-ognibuild Changes-By: lintian-brush Fixes: lintian: required-field See-also: https://lintian.debian.org/tags/required-field.html --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index 2afb26e..0757058 100644 --- a/debian/control +++ b/debian/control @@ -5,3 +5,4 @@ Source: python-ognibuild Package: python3-python-ognibuild Architecture: all +Description: Detect and run any build system From 99a5c91a1236933537800e17289ad3f85c6a7b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:00:48 +0000 Subject: [PATCH 04/40] Add missing ${misc:Depends} to Depends for python3-python-ognibuild. Changes-By: lintian-brush Fixes: lintian: debhelper-but-no-misc-depends See-also: https://lintian.debian.org/tags/debhelper-but-no-misc-depends.html --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index 0757058..bb97b0c 100644 --- a/debian/control +++ b/debian/control @@ -5,4 +5,5 @@ Source: python-ognibuild Package: python3-python-ognibuild Architecture: all +Depends: ${misc:Depends} Description: Detect and run any build system From 5d2d2098558a77a0b29e7e02ef683f905ee94ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:00:49 +0000 Subject: [PATCH 05/40] Upgrade to newer source format 3.0 (quilt). Changes-By: lintian-brush Fixes: lintian: missing-debian-source-format See-also: https://lintian.debian.org/tags/missing-debian-source-format.html Fixes: lintian: older-source-format See-also: https://lintian.debian.org/tags/older-source-format.html --- debian/source/format | 1 + 1 file changed, 1 insertion(+) create mode 100644 debian/source/format diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) From f2325ceef04a74edcc26a1d7867600d004796312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:00:49 +0000 Subject: [PATCH 06/40] Set field Upstream-Name in debian/copyright. Changes-By: lintian-brush --- debian/copyright | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/copyright b/debian/copyright index 5e60a61..5f1439a 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,4 +1,5 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: ognibuild Files: * Copyright: Unknown From 0aec8603999f8257ca5581b239014fcabe3c6b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:00:50 +0000 Subject: [PATCH 07/40] Add debian/watch file, using pypi. Changes-By: lintian-brush Fixes: lintian: debian-watch-file-is-missing See-also: https://lintian.debian.org/tags/debian-watch-file-is-missing.html --- debian/watch | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 debian/watch diff --git a/debian/watch b/debian/watch new file mode 100644 index 0000000..6082aa8 --- /dev/null +++ b/debian/watch @@ -0,0 +1,2 @@ +version=4 +opts=pgpsigurlmangle=s/$/.asc/ https://pypi.debian.net/ognibuild/ognibuild-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) From 33a297aedfc473cc3f1dfbdc6dc24f7184b910d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:00:50 +0000 Subject: [PATCH 08/40] Set priority in source stanza, since it is the same for all packages. Changes-By: lintian-brush Fixes: lintian: recommended-field See-also: https://lintian.debian.org/tags/recommended-field.html --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index bb97b0c..28a3bb8 100644 --- a/debian/control +++ b/debian/control @@ -2,6 +2,7 @@ Rules-Requires-Root: no Standards-Version: 4.5.1 Build-Depends: debhelper-compat (= 12), dh-sequence-python3 Source: python-ognibuild +Priority: optional Package: python3-python-ognibuild Architecture: all From 86031ff63b4177efb176f5fe7bb3423bdb0fb7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:00:50 +0000 Subject: [PATCH 09/40] Section field set in source based on binary package names. Changes-By: lintian-brush Fixes: lintian: recommended-field See-also: https://lintian.debian.org/tags/recommended-field.html --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index 28a3bb8..2a35c3f 100644 --- a/debian/control +++ b/debian/control @@ -3,6 +3,7 @@ Standards-Version: 4.5.1 Build-Depends: debhelper-compat (= 12), dh-sequence-python3 Source: python-ognibuild Priority: optional +Section: python Package: python3-python-ognibuild Architecture: all From cbf08a8e3f3659a1e5a7a94373a9b920560d3ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:00:50 +0000 Subject: [PATCH 10/40] =?UTF-8?q?Set=20the=20maintainer=20field=20to:=20Je?= =?UTF-8?q?lmer=20Vernoo=C4=B3=20.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes-By: lintian-brush Fixes: lintian: required-field See-also: https://lintian.debian.org/tags/required-field.html --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index 2a35c3f..e9a12e6 100644 --- a/debian/control +++ b/debian/control @@ -4,6 +4,7 @@ Build-Depends: debhelper-compat (= 12), dh-sequence-python3 Source: python-ognibuild Priority: optional Section: python +Maintainer: Jelmer Vernooij Package: python3-python-ognibuild Architecture: all From 182c8b2b554fd168833912e56a53bc2aee99702b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:05:13 +0000 Subject: [PATCH 11/40] Update copyright. --- debian/changelog | 2 +- debian/control | 15 +++++++++++++-- debian/copyright | 30 ++++++++++++++++++------------ 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/debian/changelog b/debian/changelog index c039f17..b429a1b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,5 +1,5 @@ python-ognibuild (0.0.1~git20201031.4cbc8df-1) UNRELEASED; urgency=low - * Initial release. + * Initial release. Closes: #981913 -- Jelmer Vernooij Fri, 05 Feb 2021 03:00:40 +0000 diff --git a/debian/control b/debian/control index e9a12e6..0cdd33d 100644 --- a/debian/control +++ b/debian/control @@ -1,12 +1,23 @@ Rules-Requires-Root: no Standards-Version: 4.5.1 -Build-Depends: debhelper-compat (= 12), dh-sequence-python3 +Build-Depends: debhelper-compat (= 12), dh-sequence-python3, python3-all Source: python-ognibuild Priority: optional Section: python Maintainer: Jelmer Vernooij +Vcs-Git: https://salsa.debian.org/jelmer/ognibuild.git +Vcs-Browser: https://salsa.debian.org/jelmer/ognibuild +Homepage: https://github.com/jelmer/ognibuild Package: python3-python-ognibuild Architecture: all -Depends: ${misc:Depends} +Depends: ${misc:Depends}, ${python3:Depends} 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. diff --git a/debian/copyright b/debian/copyright index 5f1439a..fbf06a4 100644 --- a/debian/copyright +++ b/debian/copyright @@ -2,21 +2,27 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: ognibuild Files: * -Copyright: Unknown -License: GPL-2+ -Comment: No explicit license found, using license(s) from: - LICENSE - -Files: LICENSE build/* dist/ognibuild-0.0.1.tar.gz ognibuild/* -Copyright: 1989-1991, Free Software Foundation, Inc - 2019-2020, Jelmer Vernooij +Copyright: Jelmer Vernooij License: GPL-2+ Files: debian/* -Copyright: Unknown +Copyright: Jelmer Vernooij License: GPL-2+ -Comment: No explicit license found, using license(s) from: - LICENSE License: GPL-2+ -Comment: Add the corresponding license text here + 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. From 51683dcee3489bb1d7670cd4909f8e1e65ef0b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:05:21 +0000 Subject: [PATCH 12/40] Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, Repository-Browse. Changes-By: lintian-brush Fixes: lintian: upstream-metadata-file-is-missing See-also: https://lintian.debian.org/tags/upstream-metadata-file-is-missing.html Fixes: lintian: upstream-metadata-missing-bug-tracking See-also: https://lintian.debian.org/tags/upstream-metadata-missing-bug-tracking.html Fixes: lintian: upstream-metadata-missing-repository See-also: https://lintian.debian.org/tags/upstream-metadata-missing-repository.html --- debian/changelog | 2 ++ debian/upstream/metadata | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 debian/upstream/metadata diff --git a/debian/changelog b/debian/changelog index b429a1b..65bc595 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,5 +1,7 @@ python-ognibuild (0.0.1~git20201031.4cbc8df-1) UNRELEASED; urgency=low * Initial release. Closes: #981913 + * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, + Repository-Browse. -- Jelmer Vernooij Fri, 05 Feb 2021 03:00:40 +0000 diff --git a/debian/upstream/metadata b/debian/upstream/metadata new file mode 100644 index 0000000..5bcc1f3 --- /dev/null +++ b/debian/upstream/metadata @@ -0,0 +1,5 @@ +--- +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 From 46f2ea0760501a128cf3a1e804df841f1b6e0966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:05:48 +0000 Subject: [PATCH 13/40] Strip changelog. --- debian/changelog | 2 -- 1 file changed, 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index 65bc595..b429a1b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,7 +1,5 @@ python-ognibuild (0.0.1~git20201031.4cbc8df-1) UNRELEASED; urgency=low * Initial release. Closes: #981913 - * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, - Repository-Browse. -- Jelmer Vernooij Fri, 05 Feb 2021 03:00:40 +0000 From 4067699b9b9017993f836a3edb56b1f47b4aa93c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:07:59 +0000 Subject: [PATCH 14/40] Add tests. --- debian/control | 2 +- debian/tests/control | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 debian/tests/control diff --git a/debian/control b/debian/control index 0cdd33d..284b9c9 100644 --- a/debian/control +++ b/debian/control @@ -1,6 +1,6 @@ Rules-Requires-Root: no Standards-Version: 4.5.1 -Build-Depends: debhelper-compat (= 12), dh-sequence-python3, python3-all +Build-Depends: debhelper-compat (= 12), dh-sequence-python3, python3-all, python3-setuptools Source: python-ognibuild Priority: optional Section: python diff --git a/debian/tests/control b/debian/tests/control new file mode 100644 index 0000000..2a7f2e5 --- /dev/null +++ b/debian/tests/control @@ -0,0 +1,3 @@ +Test-Command: python3 setup.py test +Depends: @ +Restrictions: allow-stderr From f797ac824d0d0cb08e4982ec06109addb52c6fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:24:36 +0000 Subject: [PATCH 15/40] Fix tests. --- debian/tests/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/tests/control b/debian/tests/control index 2a7f2e5..4d415c2 100644 --- a/debian/tests/control +++ b/debian/tests/control @@ -1,3 +1,3 @@ -Test-Command: python3 setup.py test +Test-Command: python3 -m unittest ognibuild.tests.test_suite Depends: @ Restrictions: allow-stderr From 28b692602e5af89fe6a77adc1483cb472e887e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:24:48 +0000 Subject: [PATCH 16/40] releasing package python-ognibuild version 0.0.1~git20201031.4cbc8df-1 --- debian/changelog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index b429a1b..7e19896 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -python-ognibuild (0.0.1~git20201031.4cbc8df-1) UNRELEASED; urgency=low +python-ognibuild (0.0.1~git20201031.4cbc8df-1) unstable; urgency=low * Initial release. Closes: #981913 From 1043130b02bf6c1f7a801d024c0ceba76469790c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:32:26 +0000 Subject: [PATCH 17/40] Change source name to ognibuild. --- debian/changelog | 2 +- debian/control | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/debian/changelog b/debian/changelog index 7e19896..15462df 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -python-ognibuild (0.0.1~git20201031.4cbc8df-1) unstable; urgency=low +ognibuild (0.0.1~git20201031.4cbc8df-1) unstable; urgency=low * Initial release. Closes: #981913 diff --git a/debian/control b/debian/control index 284b9c9..a289db3 100644 --- a/debian/control +++ b/debian/control @@ -1,15 +1,15 @@ Rules-Requires-Root: no Standards-Version: 4.5.1 Build-Depends: debhelper-compat (= 12), dh-sequence-python3, python3-all, python3-setuptools -Source: python-ognibuild +Source: ognibuild Priority: optional -Section: python +Section: devel Maintainer: Jelmer Vernooij Vcs-Git: https://salsa.debian.org/jelmer/ognibuild.git Vcs-Browser: https://salsa.debian.org/jelmer/ognibuild Homepage: https://github.com/jelmer/ognibuild -Package: python3-python-ognibuild +Package: python-ognibuild Architecture: all Depends: ${misc:Depends}, ${python3:Depends} Description: Detect and run any build system From 7a3b52080d8cc5b23915dd71fc7d23a4e62ac0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 5 Feb 2021 03:42:18 +0000 Subject: [PATCH 18/40] Fix tests. --- debian/control | 2 +- debian/tests/control | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/control b/debian/control index a289db3..dff517f 100644 --- a/debian/control +++ b/debian/control @@ -9,7 +9,7 @@ Vcs-Git: https://salsa.debian.org/jelmer/ognibuild.git Vcs-Browser: https://salsa.debian.org/jelmer/ognibuild Homepage: https://github.com/jelmer/ognibuild -Package: python-ognibuild +Package: ognibuild Architecture: all Depends: ${misc:Depends}, ${python3:Depends} Description: Detect and run any build system diff --git a/debian/tests/control b/debian/tests/control index 4d415c2..36a3ddb 100644 --- a/debian/tests/control +++ b/debian/tests/control @@ -1,3 +1,3 @@ Test-Command: python3 -m unittest ognibuild.tests.test_suite -Depends: @ +Depends: @, python3 Restrictions: allow-stderr From b46bde6c3482d252057d38cb4fd3dda2ca9a77dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 28 Feb 2021 14:51:09 +0000 Subject: [PATCH 19/40] Add dependency on buildlog-consultant. --- debian/changelog | 1 + debian/control | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 5bed804..13fc620 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,6 +1,7 @@ ognibuild (0.0.1~git20210228.2528295-1) UNRELEASED; urgency=medium * New upstream snapshot. + * Add dependency on buildlog-consultant. -- Jelmer Vernooij Sun, 28 Feb 2021 14:49:57 +0000 diff --git a/debian/control b/debian/control index dff517f..954023f 100644 --- a/debian/control +++ b/debian/control @@ -1,6 +1,6 @@ Rules-Requires-Root: no Standards-Version: 4.5.1 -Build-Depends: debhelper-compat (= 12), dh-sequence-python3, python3-all, python3-setuptools +Build-Depends: debhelper-compat (= 12), dh-sequence-python3, python3-all, python3-setuptools, python3-buildlog-consultant Source: ognibuild Priority: optional Section: devel From c6b512db5f647b6c22245bee800dbd9e07ff5f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 28 Feb 2021 14:52:16 +0000 Subject: [PATCH 20/40] Wrap-and-sort. --- debian/control | 16 ++++++++++++++-- debian/tests/control | 3 ++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/debian/control b/debian/control index 954023f..863f316 100644 --- a/debian/control +++ b/debian/control @@ -1,6 +1,13 @@ Rules-Requires-Root: no Standards-Version: 4.5.1 -Build-Depends: debhelper-compat (= 12), dh-sequence-python3, python3-all, python3-setuptools, python3-buildlog-consultant +Build-Depends: debhelper-compat (= 12), + dh-sequence-python3, + python3-all, + python3-apt, + python3-breezy, + python3-buildlog-consultant, + python3-debmutate, + python3-setuptools, Source: ognibuild Priority: optional Section: devel @@ -11,7 +18,12 @@ Homepage: https://github.com/jelmer/ognibuild Package: ognibuild Architecture: all -Depends: ${misc:Depends}, ${python3:Depends} +Depends: python3-apt, + python3-breezy, + python3-buildlog-consultant, + python3-debmutate, + ${misc:Depends}, + ${python3:Depends}, Description: Detect and run any build system Ognibuild is a simple wrapper with a common interface for invoking any kind of build tool. diff --git a/debian/tests/control b/debian/tests/control index 36a3ddb..bf97522 100644 --- a/debian/tests/control +++ b/debian/tests/control @@ -1,3 +1,4 @@ Test-Command: python3 -m unittest ognibuild.tests.test_suite -Depends: @, python3 +Depends: python3, + @, Restrictions: allow-stderr From bbf98a0550a40437cc4a8c5543bc0a0f8f260ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 28 Feb 2021 14:55:03 +0000 Subject: [PATCH 21/40] releasing package ognibuild version 0.0.1~git20210228.bc79314-1 --- debian/changelog | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/debian/changelog b/debian/changelog index 63370ee..c9dce73 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,10 +1,9 @@ -ognibuild (0.0.1~git20210228.bc79314-1) UNRELEASED; urgency=medium +ognibuild (0.0.1~git20210228.bc79314-1) unstable; urgency=medium * New upstream snapshot. * Add dependency on buildlog-consultant. - * New upstream snapshot. - -- Jelmer Vernooij Sun, 28 Feb 2021 14:53:59 +0000 + -- Jelmer Vernooij Sun, 28 Feb 2021 14:55:03 +0000 ognibuild (0.0.1~git20201031.4cbc8df-1) unstable; urgency=low From 8427e59fd8c558e095c115cde0a6c0cd93c42b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Tue, 2 Mar 2021 18:26:37 +0000 Subject: [PATCH 22/40] Add missing dependnecies. --- debian/changelog | 4 ++-- debian/control | 5 +++++ debian/tests/control | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index 29d6438..111f2e5 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -ognibuild (0.0.2-1) UNRELEASED; urgency=medium +ognibuild (0.0.2-1) unstable; urgency=medium * New upstream release. - -- Jelmer Vernooij Tue, 02 Mar 2021 17:55:58 +0000 + -- Jelmer Vernooij Tue, 02 Mar 2021 18:25:45 +0000 ognibuild (0.0.1~git20210228.bc79314-1) unstable; urgency=medium diff --git a/debian/control b/debian/control index 863f316..5276853 100644 --- a/debian/control +++ b/debian/control @@ -5,9 +5,13 @@ Build-Depends: debhelper-compat (= 12), python3-all, python3-apt, python3-breezy, + python3-breezy.tests, python3-buildlog-consultant, + python3-requirement-parser, python3-debmutate, python3-setuptools, + lintian-brush, + brz-debian Source: ognibuild Priority: optional Section: devel @@ -22,6 +26,7 @@ Depends: python3-apt, python3-breezy, python3-buildlog-consultant, python3-debmutate, + python3-requirement-parser, ${misc:Depends}, ${python3:Depends}, Description: Detect and run any build system diff --git a/debian/tests/control b/debian/tests/control index bf97522..dcef085 100644 --- a/debian/tests/control +++ b/debian/tests/control @@ -1,4 +1,5 @@ Test-Command: python3 -m unittest ognibuild.tests.test_suite Depends: python3, @, + lintian-brush, brz-debian, python3-breezy.tests Restrictions: allow-stderr From 8cba5a2b4b11f044c085026d016981ce3a46c5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Wed, 3 Mar 2021 16:42:30 +0000 Subject: [PATCH 23/40] Add missing dependency on python3-lz4. --- debian/changelog | 6 ++++++ debian/control | 2 ++ 2 files changed, 8 insertions(+) diff --git a/debian/changelog b/debian/changelog index 111f2e5..0a54dfe 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +ognibuild (0.0.2-2) UNRELEASED; urgency=medium + + * Add missing dependency on python3-lz4. + + -- Jelmer Vernooij Wed, 03 Mar 2021 16:42:28 +0000 + ognibuild (0.0.2-1) unstable; urgency=medium * New upstream release. diff --git a/debian/control b/debian/control index 5276853..4a431f9 100644 --- a/debian/control +++ b/debian/control @@ -9,6 +9,7 @@ Build-Depends: debhelper-compat (= 12), python3-buildlog-consultant, python3-requirement-parser, python3-debmutate, + python3-lz4, python3-setuptools, lintian-brush, brz-debian @@ -26,6 +27,7 @@ Depends: python3-apt, python3-breezy, python3-buildlog-consultant, python3-debmutate, + python3-lz4, python3-requirement-parser, ${misc:Depends}, ${python3:Depends}, From 67c778ed89c664192b18d06945d8971e31afc42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Wed, 3 Mar 2021 16:42:51 +0000 Subject: [PATCH 24/40] Set upstream metadata fields: Security-Contact. Changes-By: lintian-brush --- debian/changelog | 1 + debian/upstream/metadata | 1 + 2 files changed, 2 insertions(+) diff --git a/debian/changelog b/debian/changelog index 0a54dfe..d4fc461 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,6 +1,7 @@ ognibuild (0.0.2-2) UNRELEASED; urgency=medium * Add missing dependency on python3-lz4. + * Set upstream metadata fields: Security-Contact. -- Jelmer Vernooij Wed, 03 Mar 2021 16:42:28 +0000 diff --git a/debian/upstream/metadata b/debian/upstream/metadata index 5bcc1f3..6a60ea0 100644 --- a/debian/upstream/metadata +++ b/debian/upstream/metadata @@ -3,3 +3,4 @@ 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 From c36da57c75002a24656499961bbaea0305b09d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Wed, 3 Mar 2021 18:13:52 +0000 Subject: [PATCH 25/40] Add upstream signing keys. Changes-By: lintian-brush Fixes: lintian: debian-watch-file-pubkey-file-is-missing See-also: https://lintian.debian.org/tags/debian-watch-file-pubkey-file-is-missing.html --- debian/changelog | 1 + debian/upstream/signing-key.asc | 680 ++++++++++++++++++++++++++++++++ 2 files changed, 681 insertions(+) create mode 100644 debian/upstream/signing-key.asc diff --git a/debian/changelog b/debian/changelog index d4fc461..4e0d992 100644 --- a/debian/changelog +++ b/debian/changelog @@ -2,6 +2,7 @@ ognibuild (0.0.2-2) UNRELEASED; urgency=medium * Add missing dependency on python3-lz4. * Set upstream metadata fields: Security-Contact. + * Add upstream signing keys. -- Jelmer Vernooij Wed, 03 Mar 2021 16:42:28 +0000 diff --git a/debian/upstream/signing-key.asc b/debian/upstream/signing-key.asc new file mode 100644 index 0000000..bee2dd6 --- /dev/null +++ b/debian/upstream/signing-key.asc @@ -0,0 +1,680 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +AAoJEACAbyvXKaRXcQ0P/23tOfMis8J0KPI+QymR/dYj3I0xbXm3qCNVMeznnos4 +t/LQnsMvlgVfjkHaE6ScK9iOd7LqXEHDKPNOAbCLy4kyp9Ylw/hjCM5zR/S84wX3 +QdPsTvzdYwtmPr3zSbbkRK3vEKHQWyofXSFqPSeEJs6WAAWswHRKGDxL01tZTD8t +EJNGMwrnguTB+wnwiLjizjLLDy1EV7zKlzBaS2onbHBWov57cV9MqJuIu8+LK3jX +ApLPXebUZm4ywq0c3LblJu1YQyb2yKtzJxY9AiV9Oe7Kri7KP3v8Qa+UeI79UABh +WVH45aWOurdJx33XzAmYKkIKPa2Md9e4/uf5vHj8W9CILYhoudo8ko1cnMNiFqDR +bYZN1VG55ujzoT+AqeI6GKXTJ+9rzYhJtJZU7eUt58sbQH8Pm3sp4WBNUpMo36go +fHg2YJU2Pedn7TogRIhcWeU8ZiFYFNrsjCmj2hfKoK5Z7b50ywQokdnhXVWtb05D ++BVXrO7CPIt1mjMQpV2xgAear8DUThcsN/n6T6SrsIbgBfYkAAKdO4Qra4bmZmdv +Q323rYvu0MPwsciYReJ6i3qosoI4rUKkxZb3QbZ0OY0DkthVy/U6PmL+nJvsFQBP +jLqKhuGR4Itswbfh1Zn+VQ/qPUnPjR+yNlCMKnRNdT5GWn7XA6hlq3rQnYU3VYfo +tCVKZWxtZXIgVmVybm9vaWogPGplbG1lckBubC5saW51eC5vcmc+iQI8BDABAgAm +BQJNb7RSHx0gbmwubGludXgub3JnIG5vIGxvbmdlciBleGlzdHMACgkQAIBvK9cp +pFcOOw//VRlFHgGefSIkxg381wo8t38g7NXbLwGtjxvJjnrwnxQSVnesGxpZHgyo +/Tq0Ly+PPYtPzPUX2rbVw90Jx6gD2Yl7I6rnYoNurNhJqGsGRIyCQ11UdKmDu5jg +PEurEjycxZK1UOkqnfrVeCubDilFNgTuvOQaC1rw51M00sqv/8KvkhWE4ZGkvgOe +GKLe5NbxNuBTiPOyRIVWJD2q42LCaayYrcM/1cIZk55dMePiSCHkDLg0GYMdkbhs +ohE8YBr3PWm5r6LGI15ihm43YawM4kVdgSbvDB5kn11jNuOeMLqVT8VCfhN8V9J/ +oPfd8n4EZchvwiupGCi8iR5uwWIg4WOQL8dT/uuCPM0fZeZYU5oxcvjmT48FSTzZ +HOfXg8HhzhlNap4tQS+cvHleUmDvImXo/Dn9L20v5OgwBu/Wq8lREBVZjuEUxp4a ++G9v6SMhtYkFetWnAvCui3DOQKlmiL6GKpUOx3ZbXBe9uSsjMsahhhHjyc4MRsFj +4BcxCc3IMK/p8RWvrrl3l9bJlQrRsxWbT95fVPnUIqG7O+8Ttxl+oIuubnKBn3nZ +8lWBbgWbM7PeVHJNyfV91KQp/xaHV0sWLlwLmN0S5GAuqhN7gyPMHdCgfLuUN42i +AkUqehVSfJyFX0qCaoqUiIeLDrtC1f5jmbR4maiIuuUmv/p6vqu0JkplbG1lciBW +ZXJub29paiA8amVsbWVyQGNhbm9uaWNhbC5jb20+iQIfBDABAgAJBQJQlSLBAh0g +AAoJEACAbyvXKaRXQlcP/A1iUm2F6If39VmiYUa3ZA9ESi3ABtXc9OatlKzFlw/4 +nL4QVOCm9T+i0SO8Vm7IIMv76BIUgIo+JzkTpq0K7HP83rLTHjyKzkQlkMMITXYl +tzOhfLGZHgqlJZn1GNWkEuQK0ggrkqJbIWRVYepd8IgPGgK1Dlu1y+BSSRkeHR90 +6n2OasIIITgySbskEUjf+XukB2O9vzTVS8zT1Y3nUlCim6RRwhPBMPKrwSrCssCr +r24Lri6gwyrhZb3FaXVmmvHC4PNjf7jUHdyUpp7FAZ2v5ghQc9OCXLelO0QW6wnp +4TFEFvpmw910Du1fA7EnWR7kFlGaQdgifr9hQYDi9AcuEuu2+PfEx58WcLoc/+dB +4PWMX+AopVbLW/xtXNDxPrKgb96mUPd0OYQLpl5SX7B+PEk/GbuUfda54d3lmLq/ +G1v9r7bRI+r4do3ImDFUyZ/adFajP0jNApQdYmdutFWMtZNVuxvzCq/wXS9Grbft +llD96xHvoiTR1R1QF5VxGvsWVOSBcvnq+kglaVa18MwhPBiCdFJM0xRlJoqvlXJn +k4Sg8DscMGrxVgxQ/HCT1yiud+mShh7ClQlGIB+DPcyn4YYzrbKXVV23telJjMeM +KLbdBllCktaUCal2g4gxa+9zCD69pxS++usrLOeuYo7rktpRGxZ2T/7bzkQPznxr +tCdKZWxtZXIgVmVybm9vaWogPGplbG1lckBvcGVuY2hhbmdlLm9yZz6JAmoEEwEK +AFQCGwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AVGGhrcDovL2tleXMuZ251cGcu +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 +td7UCWj9i23IkX40AUJEf9z+xS2B6sdtvHdDiSezfb6KNRabV2cHLzb8IBe2WX8+ +c3y2ka0qDaKwStznA8c99enC2mDC4stQdKeSVCwWAh2KvT7ZGi4yPN1Mpx1CplKD ++luMo23FFlC6Imn5Kdy6M8NhjL7XL44eAo65E7Za9dyUvYWxSNINAujzCVCGh95w +6YrH4aXoLQBnVJC+TxRcqo4brqOScir+Ir2JjQbxS8cm33zoFc5yo0ebV4bdOEF+ +Qux5J8Ev11qWxzsu9dCP1uBO5Bw+CEp65/GVl1LIiQIlBBgBCgAPAhsgBQJXZFZl +BQkGRxBmAAoJEACAbyvXKaRXKGUQAITBRaXp7kqjfNV0ORNt7Aj6FTQb7fu5RWTC +G2HKWNiSQwfk8bUJB3yW+yHzKP455rtZqBNkTZ4+snVuzZwOjwnqIk3FbLwThGYr +lQDOpxLjqWFYa+HGl/e/8KaVKXHZu/gxB12T7qcVWxYf6AbQox9ivU/flaQNMB6o +BmtGC8jZLujN2LWsSoee2ScOfmwCgh7w3ELtfHVoKi48ciiUdJXIg6uZL5Jompfb +AgSEVJVw0c/CeZHnUgskQZlzEkgn4b+qZiq+5kX13pZwgppq/LDA5vrGi4B8N/Wq +TrHGrm/ER4FdOecrNMD05OSAkgL4kgGwDfLF0NKIZmu6crzxWEWhZVB+ifUsn1A0 +lP3ipAd1LELARUswpMRIpMGh3Xu9tLHnG7lGF4zvrCL8TPucgZdPUMDxUdRn2AJH +Qz1O+FtCYGieJGAiIt88lMZGfmayjbFAyIoiPdxUbktezpdgSWuSw8bxUHYwemfh +dgz2Mz/ctfuRVbwPr0V7g5untIm+VvONFE2m9sC61l3haHzJdXkDk7JUcxW0JI39 +6GRS+4kXb/+o5EiymeAHDJnoFViuq0oUaoKhd17MO/Oof5+XqPigM1oMKLCVBxA7 +3+Bd9KQzpxpjh/ywmL7sM+Ke69Lg/zE5kI17aM/K6g+9KzgQcU6GvY2b8HVeKgR5 +TgVoW9NHuQENBFTfrjIBCADNQ9HDQjm8JgBPbMlDeSgxjpfuDdmSiM804lVg0peh +x1Ba6TKTUuONiAw66HvapQp0eGJyexZ44CWJr/0wLgON3q3lDUqv8U0LZrn924fR +IOmrWLLiDYzvM32hKt34fDqGgBYWiO6wr7OBsr2EKI/nn1xN+Z5ulmssjtYzNL0z +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 +=uqnI +-----END PGP PUBLIC KEY BLOCK----- From 70ce8a5c4b63c45f3dde243979f3fcbd5a8e957e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sat, 27 Mar 2021 17:42:12 +0000 Subject: [PATCH 26/40] New upstream release. --- debian/changelog | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index 4e0d992..74b97da 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,10 +1,11 @@ -ognibuild (0.0.2-2) UNRELEASED; urgency=medium +ognibuild (0.0.3-1) UNRELEASED; urgency=medium * Add missing dependency on python3-lz4. * Set upstream metadata fields: Security-Contact. * Add upstream signing keys. + * New upstream release. - -- Jelmer Vernooij Wed, 03 Mar 2021 16:42:28 +0000 + -- Jelmer Vernooij Sat, 27 Mar 2021 17:42:06 +0000 ognibuild (0.0.2-1) unstable; urgency=medium From a752732e3a46aa5311b3b6916a820e831a0336fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sat, 27 Mar 2021 17:43:30 +0000 Subject: [PATCH 27/40] Add extra dependencies. --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 4a431f9..e05c64c 100644 --- a/debian/control +++ b/debian/control @@ -11,7 +11,6 @@ Build-Depends: debhelper-compat (= 12), python3-debmutate, python3-lz4, python3-setuptools, - lintian-brush, brz-debian Source: ognibuild Priority: optional @@ -29,6 +28,7 @@ Depends: python3-apt, python3-debmutate, python3-lz4, python3-requirement-parser, + python3-toml, ${misc:Depends}, ${python3:Depends}, Description: Detect and run any build system From 50875787cca7938a563736e5602b3c5916da7b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sat, 27 Mar 2021 17:44:15 +0000 Subject: [PATCH 28/40] Bump deps. --- debian/control | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/control b/debian/control index e05c64c..56f6812 100644 --- a/debian/control +++ b/debian/control @@ -6,7 +6,7 @@ Build-Depends: debhelper-compat (= 12), python3-apt, python3-breezy, python3-breezy.tests, - python3-buildlog-consultant, + python3-buildlog-consultant (>= 0.0.4), python3-requirement-parser, python3-debmutate, python3-lz4, @@ -24,7 +24,7 @@ Package: ognibuild Architecture: all Depends: python3-apt, python3-breezy, - python3-buildlog-consultant, + python3-buildlog-consultant (>= 0.0.4), python3-debmutate, python3-lz4, python3-requirement-parser, From 87bf6203e53ef70579996d14637297be015375a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sat, 27 Mar 2021 17:49:16 +0000 Subject: [PATCH 29/40] Move debian-specific dependencies to Recommends. --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 56f6812..bd6dd2e 100644 --- a/debian/control +++ b/debian/control @@ -25,12 +25,12 @@ Architecture: all Depends: python3-apt, python3-breezy, python3-buildlog-consultant (>= 0.0.4), - python3-debmutate, 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. From 722796c0aa04adca53a9b5090a473a92aa8242aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sat, 27 Mar 2021 17:49:29 +0000 Subject: [PATCH 30/40] releasing package ognibuild version 0.0.3-1 --- debian/changelog | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index 74b97da..e386b65 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,11 +1,11 @@ -ognibuild (0.0.3-1) UNRELEASED; urgency=medium +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 Sat, 27 Mar 2021 17:42:06 +0000 + -- Jelmer Vernooij Sat, 27 Mar 2021 17:49:29 +0000 ognibuild (0.0.2-1) unstable; urgency=medium From 86f809d4309bba97a0776e1c5007d3ec58a872a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Wed, 7 Apr 2021 00:11:09 +0100 Subject: [PATCH 31/40] releasing package ognibuild version 0.0.4-1 --- debian/changelog | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index 471c7bd..aa06ce2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,7 +1,7 @@ -ognibuild (0.0.4-1) UNRELEASED; urgency=low +ognibuild (0.0.4-1) unstable; urgency=low * New upstream release. - -- Jelmer Vernooij Wed, 07 Apr 2021 00:10:53 -0000 + -- Jelmer Vernooij Wed, 07 Apr 2021 00:11:09 +0100 ognibuild (0.0.3-1) unstable; urgency=medium From 40fbcfcff747ce57fe6ed4c01649f314c8d5a681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 16 May 2021 17:21:26 +0100 Subject: [PATCH 32/40] Add missing build dependency on python3-dulwich (>= 0.19.12). --- debian/control | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/control b/debian/control index bd6dd2e..cb11c07 100644 --- a/debian/control +++ b/debian/control @@ -11,7 +11,8 @@ Build-Depends: debhelper-compat (= 12), python3-debmutate, python3-lz4, python3-setuptools, - brz-debian + brz-debian, + python3-dulwich (>= 0.19.12) Source: ognibuild Priority: optional Section: devel From 50bd51219fa3c752f7a65d9b4b9f206782538810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Tue, 18 May 2021 01:34:11 +0100 Subject: [PATCH 33/40] releasing package ognibuild version 0.0.5-1 --- debian/changelog | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index cbd8612..bd35f86 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,10 +1,10 @@ -ognibuild (0.0.5-1) UNRELEASED; urgency=low +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 Sun, 16 May 2021 17:08:54 -0000 + -- Jelmer Vernooij Tue, 18 May 2021 01:34:11 +0100 ognibuild (0.0.4-1) unstable; urgency=low From b59ac3355ff8c07adf0081e4baff5ff4b2c8bd08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Tue, 18 May 2021 20:53:15 +0100 Subject: [PATCH 34/40] releasing package ognibuild version 0.0.6+git20210517.1.8189e91-1 --- debian/changelog | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index f5eb88b..711ed49 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,8 @@ -ognibuild (0.0.6+git20210517.1.8189e91-1) UNRELEASED; urgency=low +ognibuild (0.0.6+git20210517.1.8189e91-1) unstable; urgency=low * New upstream snapshot. - -- Jelmer Vernooij Tue, 18 May 2021 20:53:00 -0000 + -- Jelmer Vernooij Tue, 18 May 2021 20:53:15 +0100 ognibuild (0.0.5-1) unstable; urgency=low From c286789e3772970f7f2b5f70564a8545e4f0f0b8 Mon Sep 17 00:00:00 2001 From: Tianyu Chen Date: Tue, 22 Nov 2022 11:19:55 +0800 Subject: [PATCH 35/40] New upstream version 0.0.15 --- .github/workflows/disperse.yml | 24 + .github/workflows/pythonpackage.yml | 27 +- .gitignore | 1 + Makefile | 20 + PKG-INFO | 17 - README.md | 6 + disperse.conf | 8 + ognibuild.egg-info/PKG-INFO | 17 - ognibuild.egg-info/SOURCES.txt | 52 - ognibuild.egg-info/dependency_links.txt | 1 - ognibuild.egg-info/entry_points.txt | 4 - ognibuild.egg-info/requires.txt | 8 - ognibuild.egg-info/top_level.txt | 1 - ognibuild/__init__.py | 70 +- ognibuild/__main__.py | 175 +++- ognibuild/build.py | 16 +- ognibuild/buildlog.py | 244 +++-- ognibuild/buildsystem.py | 903 ++++++++++++------ ognibuild/clean.py | 13 +- ognibuild/debian/__init__.py | 3 +- ognibuild/debian/apt.py | 27 +- ognibuild/debian/build.py | 195 ++-- ognibuild/debian/build_deps.py | 45 +- ognibuild/debian/file_search.py | 88 +- ognibuild/debian/fix_build.py | 284 +++--- ognibuild/debian/udd.py | 6 +- ognibuild/dep_server.py | 126 +++ ognibuild/dist.py | 218 +++-- ognibuild/dist_catcher.py | 13 +- ognibuild/fix_build.py | 49 +- ognibuild/fixers.py | 34 + ognibuild/info.py | 12 +- ognibuild/install.py | 19 +- ognibuild/logs.py | 105 ++ ognibuild/requirements.py | 325 +++++-- ognibuild/resolver/__init__.py | 153 ++- ognibuild/resolver/apt.py | 641 ++++++++----- ognibuild/resolver/dep_server.py | 88 ++ ognibuild/session/__init__.py | 22 +- ognibuild/session/plain.py | 14 +- ognibuild/session/schroot.py | 44 +- ognibuild/test.py | 13 +- ognibuild/upstream.py | 253 +++++ ognibuild/vcs.py | 3 +- pyproject.toml | 3 + releaser.conf | 14 - scripts/report-apt-deps-status | 89 ++ setup.cfg | 62 +- setup.py | 41 +- {ognibuild/tests => tests}/__init__.py | 7 +- tests/test_buildlog.py | 47 + .../tests => tests}/test_debian_build.py | 51 +- .../tests => tests}/test_debian_fix_build.py | 106 +- tests/test_logs.py | 95 ++ tests/test_resolver_apt.py | 47 + 55 files changed, 3578 insertions(+), 1371 deletions(-) create mode 100644 .github/workflows/disperse.yml create mode 100644 Makefile delete mode 100644 PKG-INFO create mode 100644 disperse.conf delete mode 100644 ognibuild.egg-info/PKG-INFO delete mode 100644 ognibuild.egg-info/SOURCES.txt delete mode 100644 ognibuild.egg-info/dependency_links.txt delete mode 100644 ognibuild.egg-info/entry_points.txt delete mode 100644 ognibuild.egg-info/requires.txt delete mode 100644 ognibuild.egg-info/top_level.txt create mode 100644 ognibuild/dep_server.py create mode 100644 ognibuild/logs.py create mode 100644 ognibuild/resolver/dep_server.py create mode 100644 ognibuild/upstream.py create mode 100644 pyproject.toml delete mode 100644 releaser.conf create mode 100755 scripts/report-apt-deps-status rename {ognibuild/tests => tests}/__init__.py (85%) create mode 100644 tests/test_buildlog.py rename {ognibuild/tests => tests}/test_debian_build.py (72%) rename {ognibuild/tests => tests}/test_debian_fix_build.py (72%) create mode 100644 tests/test_logs.py create mode 100644 tests/test_resolver_apt.py diff --git a/.github/workflows/disperse.yml b/.github/workflows/disperse.yml new file mode 100644 index 0000000..b65bb01 --- /dev/null +++ b/.github/workflows/disperse.yml @@ -0,0 +1,24 @@ +--- +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 . diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 0725c4c..1d6b7d4 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -1,6 +1,11 @@ +--- name: Python package -on: [push, pull_request] +"on": + push: + pull_request: + schedule: + - cron: '0 6 * * *' # Daily 6AM UTC build jobs: build: @@ -9,7 +14,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: [3.7, 3.8] + python-version: [3.7, 3.8, 3.9, '3.10'] fail-fast: false steps: @@ -20,28 +25,28 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip flake8 cython + python -m pip install --upgrade pip + python -m pip install -e ".[remote,dep_server,dev]" python setup.py develop - name: Install Debian-specific dependencies run: | - sudo apt install libapt-pkg-dev - python -m pip install wheel - python -m pip install git+https://salsa.debian.org/apt-team/python-apt + 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 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 + pip install -U mypy types-toml python -m mypy ognibuild if: "matrix.python-version != 'pypy3'" - name: Test suite run run: | - python -m unittest ognibuild.tests.test_suite + python -m unittest tests.test_suite env: PYTHONHASHSEED: random diff --git a/.gitignore b/.gitignore index 13818ea..9bbe794 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.coverage build *~ ognibuild.egg-info diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0297c36 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +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 diff --git a/PKG-INFO b/PKG-INFO deleted file mode 100644 index 8ab0d39..0000000 --- a/PKG-INFO +++ /dev/null @@ -1,17 +0,0 @@ -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 diff --git a/README.md b/README.md index 5374ac2..b9e493f 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,12 @@ 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 diff --git a/disperse.conf b/disperse.conf new file mode 100644 index 0000000..4e8de5b --- /dev/null +++ b/disperse.conf @@ -0,0 +1,8 @@ +# 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" +} diff --git a/ognibuild.egg-info/PKG-INFO b/ognibuild.egg-info/PKG-INFO deleted file mode 100644 index 8ab0d39..0000000 --- a/ognibuild.egg-info/PKG-INFO +++ /dev/null @@ -1,17 +0,0 @@ -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 diff --git a/ognibuild.egg-info/SOURCES.txt b/ognibuild.egg-info/SOURCES.txt deleted file mode 100644 index 2067e75..0000000 --- a/ognibuild.egg-info/SOURCES.txt +++ /dev/null @@ -1,52 +0,0 @@ -.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 \ No newline at end of file diff --git a/ognibuild.egg-info/dependency_links.txt b/ognibuild.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/ognibuild.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/ognibuild.egg-info/entry_points.txt b/ognibuild.egg-info/entry_points.txt deleted file mode 100644 index dc61857..0000000 --- a/ognibuild.egg-info/entry_points.txt +++ /dev/null @@ -1,4 +0,0 @@ -[console_scripts] -deb-fix-build = ognibuild.debian.fix_build:main -ogni = ognibuild.__main__:main - diff --git a/ognibuild.egg-info/requires.txt b/ognibuild.egg-info/requires.txt deleted file mode 100644 index 9c36b12..0000000 --- a/ognibuild.egg-info/requires.txt +++ /dev/null @@ -1,8 +0,0 @@ -breezy -buildlog-consultant>=0.0.10 -requirements-parser - -[debian] -debmutate -python_apt -python_debian diff --git a/ognibuild.egg-info/top_level.txt b/ognibuild.egg-info/top_level.txt deleted file mode 100644 index 7e9084f..0000000 --- a/ognibuild.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -ognibuild diff --git a/ognibuild/__init__.py b/ognibuild/__init__.py index 22dfaec..bc255e1 100644 --- a/ognibuild/__init__.py +++ b/ognibuild/__init__.py @@ -18,12 +18,14 @@ import os import stat +from typing import List, Dict, Type -__version__ = (0, 0, 7) +__version__ = (0, 0, 15) +version_string = '.'.join(map(str, __version__)) -USER_AGENT = "Ognibuild" +USER_AGENT = f"Ognibuild/{version_string}" class DetailedFailure(Exception): @@ -32,6 +34,12 @@ 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.""" @@ -42,6 +50,13 @@ 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__, @@ -64,17 +79,64 @@ 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 - def __init__(self, family): - self.family = family + _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 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): diff --git a/ognibuild/__main__.py b/ognibuild/__main__.py index 27ddccd..0826eb4 100644 --- a/ognibuild/__main__.py +++ b/ognibuild/__main__.py @@ -15,11 +15,13 @@ # 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 . import UnidentifiedError, DetailedFailure +from urllib.parse import urlparse +from . import UnidentifiedError, DetailedFailure, version_string from .buildlog import ( InstallFixer, ExplainInstallFixer, @@ -29,9 +31,10 @@ from .buildlog import ( from .buildsystem import NoBuildToolsFound, detect_buildsystems from .resolver import ( auto_resolver, - native_resolvers, + select_resolvers, + UnsatisfiedRequirements, ) -from .resolver.apt import AptResolver +from .session import SessionSetupFailure def display_explain_commands(commands): @@ -39,34 +42,33 @@ 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))) - - -def get_necessary_declared_requirements(resolver, requirements, stages): - missing = [] - for stage, req in requirements: - if stage in stages: - missing.append(req) - return missing + logging.info( + " %s (to install %s)", command, ", ".join(map(str, reqs))) 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) - ) - install_missing_reqs(session, resolver, relevant, explain=explain) + 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 + ) # Types of dependencies: @@ -82,6 +84,7 @@ STAGE_MAP = { "test": ["test", "build", "core"], "build": ["build", "core"], "clean": [], + "verify": ["build", "core", "test"], } @@ -95,9 +98,13 @@ def determine_fixers(session, resolver, explain=False): def main(): # noqa: C901 import argparse - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser(prog='ogni') parser.add_argument( - "--directory", "-d", type=str, help="Directory for project.", default="." + "--version", action="version", version="%(prog)s " + version_string + ) + parser.add_argument( + "--directory", "-d", type=str, help="Directory for project.", + default="." ) parser.add_argument("--schroot", type=str, help="schroot to run in.") parser.add_argument( @@ -123,6 +130,15 @@ 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") @@ -130,12 +146,11 @@ 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') @@ -155,38 +170,72 @@ def main(): # noqa: C901 from .session.plain import PlainSession session = PlainSession() - with session: - logging.info("Preparing directory %s", args.directory) - external_dir, internal_dir = session.setup_from_directory(args.directory) + + 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) session.chdir(internal_dir) os.chdir(external_dir) if not session.is_temporary and args.subcommand == 'info': args.explain = True - 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": + if 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(session, args.subargv, fixers) + run_with_build_fixers(fixers, session, args.subargv) return 0 - bss = list(detect_buildsystems(args.directory)) + bss = list(detect_buildsystems(external_dir)) 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 @@ -207,11 +256,15 @@ 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 @@ -226,14 +279,42 @@ 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, DetailedFailure): + 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') return 1 except NoBuildToolsFound: logging.info("No build tools found.") diff --git a/ognibuild/build.py b/ognibuild/build.py index 1b03bf5..17df903 100644 --- a/ognibuild/build.py +++ b/ognibuild/build.py @@ -15,16 +15,28 @@ # 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_build(session, buildsystems, resolver, fixers): +BUILD_LOG_FILENAME = 'build.log' + + +def run_build(session, buildsystems, resolver, fixers, log_manager=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() + for buildsystem in buildsystems: - buildsystem.build(session, resolver, fixers) + iterate_with_build_fixers( + fixers, log_manager.wrap( + partial(buildsystem.build, session, resolver))) return raise NoBuildToolsFound() diff --git a/ognibuild/buildlog.py b/ognibuild/buildlog.py index 037f2c9..29bcddd 100644 --- a/ognibuild/buildlog.py +++ b/ognibuild/buildlog.py @@ -19,65 +19,32 @@ """ import logging +from typing import Optional, List, Callable, Union, Tuple from buildlog_consultant.common import ( - MissingPythonModule, - MissingPythonDistribution, - MissingCHeader, - MissingPkgConfig, - MissingCommand, - MissingFile, - MissingJavaScriptRuntime, - MissingSprocketsFile, - MissingGoPackage, + Problem, MissingPerlFile, - MissingPerlModule, - MissingXmlEntity, - MissingJDKFile, - MissingJDK, - MissingJRE, - MissingNodeModule, - MissingNodePackage, - MissingPhpClass, - MissingRubyGem, - MissingLibrary, MissingSetupPyCommand, - MissingJavaClass, - MissingCSharpCompiler, - MissingRPackage, - MissingRubyFile, - MissingAutoconfMacro, - MissingValaPackage, - MissingBoostComponents, + MissingCMakeComponents, 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, @@ -92,6 +59,7 @@ from .requirements import ( HaskellPackageRequirement, MavenArtifactRequirement, BoostComponentRequirement, + KF5ComponentRequirement, GnomeCommonRequirement, JDKFileRequirement, JDKRequirement, @@ -112,86 +80,124 @@ from .requirements import ( LatexPackageRequirement, CargoCrateRequirement, StaticLibraryRequirement, + GnulibDirectoryRequirement, + LuaModuleRequirement, + PHPExtensionRequirement, + VcsControlDirectoryAccessRequirement, + RubyGemRequirement, + QtModuleRequirement, ) from .resolver import UnsatisfiedRequirements -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) +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 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 [HaskellPackageRequirement.from_string(dep) for dep in problem.deps] + return OneOfRequirement( + [HaskellPackageRequirement.from_string(dep) + for dep in problem.deps]) elif isinstance(problem, MissingMavenArtifacts): - return [ + return OneOfRequirement([ 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: @@ -210,36 +216,20 @@ def problem_to_upstream_requirement(problem): # noqa: C901 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 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): + elif problem.kind == 'unsatisfied-apt-dependencies': from .resolver.apt import AptRequirement return AptRequirement(problem.relations) else: diff --git a/ognibuild/buildsystem.py b/ognibuild/buildsystem.py index f3cd7a0..f508219 100644 --- a/ognibuild/buildsystem.py +++ b/ognibuild/buildsystem.py @@ -20,8 +20,7 @@ import logging import os import re -import shlex -from typing import Optional, Tuple +from typing import Optional, Tuple, Type, List, Iterable import warnings from . import shebang_binary, UnidentifiedError @@ -43,8 +42,9 @@ from .requirements import ( MavenArtifactRequirement, GoRequirement, GoPackageRequirement, + VagueDependencyRequirement, ) -from .fix_build import run_with_build_fixers +from .fix_build import run_with_build_fixers, run_detecting_problems from .session import which @@ -69,6 +69,14 @@ class InstallTarget(object): # TODO(jelmer): Add information about target directory, layout, etc. +def get_necessary_declared_requirements(resolver, requirements, stages): + missing = [] + for stage, req in requirements: + if stage in stages: + missing.append(req) + return missing + + class BuildSystem(object): """A particular buildsystem.""" @@ -78,20 +86,27 @@ class BuildSystem(object): return self.name def dist( - self, session, resolver, fixers, target_directory: str, quiet=False + self, session, resolver, target_directory: str, quiet=False ) -> str: raise NotImplementedError(self.dist) - def test(self, session, resolver, fixers): + def install_declared_requirements(self, stages, session, resolver, fixers): + from .buildlog import install_missing_reqs + declared_reqs = self.get_declared_dependencies(session, fixers) + relevant = get_necessary_declared_requirements( + resolver, declared_reqs, stages) + install_missing_reqs(session, resolver, relevant, explain=False) + + def test(self, session, resolver): raise NotImplementedError(self.test) - def build(self, session, resolver, fixers): + def build(self, session, resolver): raise NotImplementedError(self.build) - def clean(self, session, resolver, fixers): + def clean(self, session, resolver): raise NotImplementedError(self.clean) - def install(self, session, resolver, fixers, install_target): + def install(self, session, resolver, install_target): raise NotImplementedError(self.install) def get_declared_dependencies(self, session, fixers=None): @@ -101,7 +116,7 @@ class BuildSystem(object): raise NotImplementedError(self.get_declared_outputs) @classmethod - def probe(cls, path): + def probe(cls, path: str) -> Optional["BuildSystem"]: return None @@ -128,23 +143,33 @@ class Pear(BuildSystem): def __init__(self, path): self.path = path - def dist(self, session, resolver, fixers, target_directory: str, quiet=False): + def dist(self, session, resolver, target_directory: str, + quiet: bool = False): with DistCatcher([session.external_path(".")]) as dc: - run_with_build_fixers(session, [guaranteed_which(session, resolver, "pear"), "package"], fixers) + run_detecting_problems( + session, + [guaranteed_which(session, resolver, "pear"), "package"]) return dc.copy_single(target_directory) - def test(self, session, resolver, fixers): - run_with_build_fixers(session, [guaranteed_which(session, resolver, "pear"), "run-tests"], fixers) + def test(self, session, resolver): + run_detecting_problems( + session, + [guaranteed_which(session, resolver, "pear"), "run-tests"]) - def build(self, session, resolver, fixers): - run_with_build_fixers(session, [guaranteed_which(session, resolver, "pear"), "build", self.path], fixers) + def build(self, session, resolver): + run_detecting_problems( + session, + [guaranteed_which(session, resolver, "pear"), "build", self.path]) - def clean(self, session, resolver, fixers): + def clean(self, session, resolver): self.setup(resolver) # TODO - def install(self, session, resolver, fixers, install_target): - run_with_build_fixers(session, [guaranteed_which(session, resolver, "pear"), "install", self.path], fixers) + def install(self, session, resolver, install_target): + run_detecting_problems( + session, + [guaranteed_which( + session, resolver, "pear"), "install", self.path]) def get_declared_dependencies(self, session, fixers=None): path = os.path.join(self.path, "package.xml") @@ -194,7 +219,9 @@ class Pear(BuildSystem): for ns in cls.PEAR_NAMESPACES: if tree.root.tag == '{%s}package' % ns: - logging.debug("Found package.xml with namespace %s, assuming pear package.") + logging.debug( + "Found package.xml with namespace %s, " + "assuming pear package.") return cls(path) @@ -203,6 +230,11 @@ class Pear(BuildSystem): def run_setup(script_name, script_args=None, stop_after="run"): + # Import setuptools, just in case it decides to replace distutils + try: + import setuptools # noqa: F401 + except ImportError: + pass from distutils import core import sys @@ -235,6 +267,10 @@ def run_setup(script_name, script_args=None, stop_after="run"): _setup_wrapper = """\ +try: + import setuptools +except ImportError: + pass import distutils from distutils import core import sys @@ -295,6 +331,9 @@ class SetupPy(BuildSystem): self.config = self.load_setup_cfg() except FileNotFoundError: self.config = None + except ModuleNotFoundError as e: + logging.warning('Error parsing setup.cfg: %s', e) + self.config = None try: self.pyproject = self.load_toml() @@ -313,7 +352,7 @@ class SetupPy(BuildSystem): return toml.load(pf) def load_setup_cfg(self): - from setuptools.config import read_configuration + from setuptools.config.setupcfg import read_configuration p = os.path.join(self.path, "setup.cfg") if os.path.exists(p): @@ -338,7 +377,8 @@ class SetupPy(BuildSystem): if d is None: logging.warning( "'distutils.core.setup()' was never called -- " - "perhaps '%s' is not a Distutils setup script?" % os.path.basename(p) + "perhaps '%s' is not a Distutils setup script?", + os.path.basename(p) ) return None @@ -357,9 +397,7 @@ class SetupPy(BuildSystem): import tempfile import json - interpreter = shebang_binary(os.path.join(self.path, "setup.py")) - if interpreter is None: - interpreter = self.DEFAULT_PYTHON + interpreter = self._determine_interpreter() output_f = tempfile.NamedTemporaryFile( dir=os.path.join(session.location, "tmp"), mode="w+t" ) @@ -369,14 +407,16 @@ class SetupPy(BuildSystem): argv = [ interpreter, "-c", - _setup_wrapper.replace("%(script_name)s", '"setup.py"').replace( - "%(output_path)s", - '"/' + os.path.relpath(output_f.name, session.location) + '"', + _setup_wrapper.replace( + "%(script_name)s", '"setup.py"').replace( + "%(output_path)s", + '"/' + os.path.relpath(output_f.name, session.location) + + '"', ), ] try: if fixers is not None: - run_with_build_fixers(session, argv, fixers) + run_with_build_fixers(fixers, session, argv, quiet=True) else: session.check_call(argv, close_fds=False) except RuntimeError as e: @@ -388,31 +428,43 @@ class SetupPy(BuildSystem): def __repr__(self): return "%s(%r)" % (type(self).__name__, self.path) - def test(self, session, resolver, fixers): + def test(self, session, resolver): if os.path.exists(os.path.join(self.path, "tox.ini")): - run_with_build_fixers(session, ["tox"], fixers) - elif self.pyproject: - run_with_build_fixers( - session, [self.DEFAULT_PYTHON, "-m", "pep517.check", "."], fixers - ) - elif self.has_setup_py: + run_detecting_problems( + session, ["tox", "--skip-missing-interpreters"]) + return + if self.pyproject: + run_detecting_problems( + session, ["python3", "-m", "pep517.check", "."]) + return + if 'tool:pytest' in self.config or 'pytest' in self.config: + run_detecting_problems(session, ['pytest']) + return + if self.has_setup_py: # Pre-emptively insall setuptools, since distutils doesn't provide # a 'test' subcommand and some packages fall back to distutils # if setuptools is not available. setuptools_req = PythonPackageRequirement("setuptools") if not setuptools_req.met(session): resolver.install([setuptools_req]) - self._run_setup(session, resolver, ["test"], fixers) - else: - raise NotImplementedError + try: + self._run_setup(session, resolver, ["test"]) + except UnidentifiedError as e: + if "error: invalid command 'test'" in e.lines: + pass + else: + raise + else: + return + raise NotImplementedError - def build(self, session, resolver, fixers): + def build(self, session, resolver): if self.has_setup_py: - self._run_setup(session, resolver, ["build"], fixers) + self._run_setup(session, resolver, ["build"]) else: raise NotImplementedError - def dist(self, session, resolver, fixers, target_directory, quiet=False): + def dist(self, session, resolver, target_directory, quiet=False): # TODO(jelmer): Look at self.build_backend if self.has_setup_py: preargs = [] @@ -424,59 +476,68 @@ class SetupPy(BuildSystem): if not setuptools_req.met(session): resolver.install([setuptools_req]) with DistCatcher([session.external_path("dist")]) as dc: - self._run_setup(session, resolver, preargs + ["sdist"], fixers) + self._run_setup(session, resolver, preargs + ["sdist"]) return dc.copy_single(target_directory) elif self.pyproject: with DistCatcher([session.external_path("dist")]) as dc: - run_with_build_fixers( + run_detecting_problems( session, - [self.DEFAULT_PYTHON, "-m", "pep517.build", "--source", "."], - fixers, + ["python3", "-m", "pep517.build", "--source", "."], ) return dc.copy_single(target_directory) raise AssertionError("no setup.py or pyproject.toml") - def clean(self, session, resolver, fixers): + def clean(self, session, resolver): if self.has_setup_py: - self._run_setup(session, resolver, ["clean"], fixers) + self._run_setup(session, resolver, ["clean"]) else: raise NotImplementedError - def install(self, session, resolver, fixers, install_target): + def install(self, session, resolver, install_target): if self.has_setup_py: extra_args = [] if install_target.user: extra_args.append("--user") if install_target.prefix: extra_args.append("--prefix=%s" % install_target.prefix) - self._run_setup(session, resolver, ["install"] + extra_args, fixers) + self._run_setup( + session, resolver, ["install"] + extra_args) else: raise NotImplementedError - def _run_setup(self, session, resolver, args, fixers): + def _determine_interpreter(self): + interpreter = None + if self.config: + python_requires = self.config.get( + 'options', {}).get('python_requires') + if python_requires: + if not python_requires.contains('2.7'): + interpreter = 'python3' + if interpreter is None: + interpreter = shebang_binary(os.path.join(self.path, "setup.py")) + if interpreter is None: + interpreter = self.DEFAULT_PYTHON + return interpreter + + def _run_setup(self, session, resolver, args): from .buildlog import install_missing_reqs # Install the setup_requires beforehand, since otherwise # setuptools might fetch eggs instead of our preferred resolver. install_missing_reqs(session, resolver, list(self._setup_requires())) - interpreter = shebang_binary(os.path.join(self.path, "setup.py")) - if interpreter is None: - interpreter = self.DEFAULT_PYTHON + interpreter = self._determine_interpreter() argv = [interpreter, "./setup.py"] + args # TODO(jelmer): Perhaps this should be additive? env = dict(os.environ) - # Inherit SETUPTOOLS_SCM_PRETEND_VERSION from the current environment - if "SETUPTOOLS_SCM_PRETEND_VERSION" in os.environ: - env["SETUPTOOLS_SCM_PRETEND_VERSION"] = os.environ[ - "SETUPTOOLS_SCM_PRETEND_VERSION" - ] - run_with_build_fixers(session, argv, fixers, env=env) + run_detecting_problems(session, argv, env=env) def _setup_requires(self): if self.pyproject: if "build-system" in self.pyproject: - for require in self.pyproject["build-system"].get("requires", []): - yield PythonPackageRequirement.from_requirement_str(require) + requires = self.pyproject["build-system"].get("requires", []) + for require in requires: + yield PythonPackageRequirement.from_requirement_str( + require) if self.config: options = self.config.get("options", {}) for require in options.get("setup_requires", []): @@ -486,28 +547,35 @@ class SetupPy(BuildSystem): distribution = self._extract_setup(session, fixers) if distribution is not None: for require in distribution["requires"]: - yield "core", PythonPackageRequirement.from_requirement_str(require) + yield "core", PythonPackageRequirement.from_requirement_str( + require) # Not present for distutils-only packages for require in distribution["setup_requires"]: - yield "build", PythonPackageRequirement.from_requirement_str(require) + yield "build", PythonPackageRequirement.from_requirement_str( + require) # Not present for distutils-only packages for require in distribution["install_requires"]: - yield "core", PythonPackageRequirement.from_requirement_str(require) + yield "core", PythonPackageRequirement.from_requirement_str( + require) # Not present for distutils-only packages for require in distribution["tests_require"]: - yield "test", PythonPackageRequirement.from_requirement_str(require) + yield "test", PythonPackageRequirement.from_requirement_str( + require) if self.pyproject: if "build-system" in self.pyproject: - for require in self.pyproject["build-system"].get("requires", []): - yield "build", PythonPackageRequirement.from_requirement_str( - require - ) + requires = self.pyproject["build-system"].get("requires", []) + for require in requires: + yield ( + "build", + PythonPackageRequirement.from_requirement_str(require)) if self.config: options = self.config.get("options", {}) for require in options.get("setup_requires", []): - yield "build", PythonPackageRequirement.from_requirement_str(require) + yield "build", PythonPackageRequirement.from_requirement_str( + require) for require in options.get("install_requires", []): - yield "core", PythonPackageRequirement.from_requirement_str(require) + yield "core", PythonPackageRequirement.from_requirement_str( + require) def get_declared_outputs(self, session, fixers=None): distribution = self._extract_setup(session, fixers) @@ -515,7 +583,8 @@ class SetupPy(BuildSystem): if distribution is not None: for script in distribution["scripts"]: yield BinaryOutput(os.path.basename(script)) - for script in distribution["entry_points"].get("console_scripts", []): + for script in distribution["entry_points"].get( + "console_scripts", []): yield BinaryOutput(script.split("=")[0]) all_packages.update(distribution["packages"]) if self.config: @@ -523,7 +592,8 @@ class SetupPy(BuildSystem): all_packages.update(options.get("packages", [])) for script in options.get("scripts", []): yield BinaryOutput(os.path.basename(script)) - for script in options.get("entry_points", {}).get("console_scripts", []): + for script in options.get("entry_points", {}).get( + "console_scripts", []): yield BinaryOutput(script.split("=")[0]) packages = set() @@ -571,11 +641,49 @@ class Bazel(BuildSystem): logging.debug("Found BUILD, assuming bazel package.") return cls(path) - def build(self, session, resolver, fixers): - run_with_build_fixers(session, ["bazel", "build", "//..."], fixers) + def build(self, session, resolver): + run_detecting_problems(session, ["bazel", "build", "//..."]) - def test(self, session, resolver, fixers): - run_with_build_fixers(session, ["bazel", "test", "//..."], fixers) + def test(self, session, resolver): + run_detecting_problems(session, ["bazel", "test", "//..."]) + + +class GnomeShellExtension(BuildSystem): + + name = "gnome-shell-extension" + + def __init__(self, path): + self.path = path + + def __repr__(self): + return "%s(%r)" % (type(self).__name__, self.path) + + @classmethod + def exists(cls, path): + if not os.path.exists(os.path.join(path, "metadata.json")): + return False + return True + + @classmethod + def probe(cls, path): + if cls.exists(path): + logging.debug( + "Found metadata.json , assuming gnome-shell extension.") + return cls(path) + + def build(self, session, resolver): + pass + + def test(self, session, resolver): + pass + + def get_declared_dependencies(self, session, fixers=None): + import json + with open(os.path.join(self.path, 'metadata.json'), 'r') as f: + metadata = json.load(f) + if 'shell-version' in metadata: + # TODO(jelmer): Somehow represent supported versions + yield "core", VagueDependencyRequirement("gnome-shell") class Octave(BuildSystem): @@ -639,9 +747,8 @@ class Gradle(BuildSystem): @classmethod def exists(cls, path): - return os.path.exists(os.path.join(path, "build.gradle")) or os.path.exists( - os.path.join(path, "build.gradle.kts") - ) + return (os.path.exists(os.path.join(path, "build.gradle")) + or os.path.exists(os.path.join(path, "build.gradle.kts"))) @classmethod def from_path(cls, path): @@ -661,7 +768,7 @@ class Gradle(BuildSystem): if not binary_req.met(session): resolver.install([binary_req]) - def _run(self, session, resolver, task, args, fixers): + def _run(self, session, resolver, task, args): self.setup(session, resolver) argv = [] if self.executable.startswith("./") and ( @@ -671,12 +778,13 @@ class Gradle(BuildSystem): argv.extend([self.executable, task]) argv.extend(args) try: - run_with_build_fixers(session, argv, fixers) + run_detecting_problems(session, argv) except UnidentifiedError as e: if any( [ re.match( - r"Task '" + task + r"' not found in root project '.*'\.", line + r"Task '" + task + + r"' not found in root project '.*'\.", line ) for line in e.lines ] @@ -684,24 +792,24 @@ class Gradle(BuildSystem): raise NotImplementedError raise - def clean(self, session, resolver, fixers): - self._run(session, resolver, "clean", [], fixers) + def clean(self, session, resolver): + self._run(session, resolver, "clean", []) - def build(self, session, resolver, fixers): - self._run(session, resolver, "build", [], fixers) + def build(self, session, resolver): + self._run(session, resolver, "build", []) - def test(self, session, resolver, fixers): - self._run(session, resolver, "test", [], fixers) + def test(self, session, resolver): + self._run(session, resolver, "test", []) - def dist(self, session, resolver, fixers, target_directory, quiet=False): + def dist(self, session, resolver, target_directory, quiet=False): with DistCatcher([session.external_path(".")]) as dc: - self._run(session, resolver, "distTar", [], fixers) + self._run(session, resolver, "distTar", []) return dc.copy_single(target_directory) - def install(self, session, resolver, fixers, install_target): + def install(self, session, resolver, install_target): raise NotImplementedError # TODO(jelmer): installDist just creates files under build/install/... - self._run(session, resolver, "installDist", [], fixers) + self._run(session, resolver, "installDist", []) class R(BuildSystem): @@ -716,31 +824,39 @@ class R(BuildSystem): def __repr__(self): return "%s(%r)" % (type(self).__name__, self.path) - def build(self, session, resolver, fixers): + def build(self, session, resolver): pass - def dist(self, session, resolver, fixers, target_directory, quiet=False): + def dist(self, session, resolver, target_directory, quiet=False): r_path = guaranteed_which(session, resolver, "R") with DistCatcher([session.external_path(".")]) as dc: - run_with_build_fixers(session, [r_path, "CMD", "build", "."], fixers) + run_detecting_problems(session, [r_path, "CMD", "build", "."]) return dc.copy_single(target_directory) - def install(self, session, resolver, fixers, install_target): + def install(self, session, resolver, install_target): extra_args = [] if install_target.prefix: extra_args.append("--prefix=%s" % install_target.prefix) r_path = guaranteed_which(session, resolver, "R") - run_with_build_fixers(session, [r_path, "CMD", "INSTALL", "."] + extra_args, fixers) + run_detecting_problems( + session, [r_path, "CMD", "INSTALL", "."] + extra_args) - def test(self, session, resolver, fixers): + def test(self, session, resolver): r_path = guaranteed_which(session, resolver, "R") - run_with_build_fixers(session, [r_path, "CMD", "check", "."], fixers) + if session.exists("run_tests.sh"): + run_detecting_problems(session, ["./run_tests.sh"]) + elif session.exists("tests/testthat"): + run_detecting_problems( + session, [r_path, "-e", "testthat::test_dir('tests')"]) + + def lint(self, session, resolver): + r_path = guaranteed_which(session, resolver, "R") + run_detecting_problems(session, [r_path, "CMD", "check"]) @classmethod def probe(cls, path): - if os.path.exists(os.path.join(path, "DESCRIPTION")) and os.path.exists( - os.path.join(path, "NAMESPACE") - ): + if (os.path.exists(os.path.join(path, "DESCRIPTION")) + and os.path.exists(os.path.join(path, "NAMESPACE"))): return cls(path) def _read_description(self): @@ -785,37 +901,39 @@ class Meson(BuildSystem): def __repr__(self): return "%s(%r)" % (type(self).__name__, self.path) - def _setup(self, session, fixers): + def _setup(self, session): if not session.exists("build"): session.mkdir("build") - run_with_build_fixers(session, ["meson", "setup", "build"], fixers) + run_detecting_problems(session, ["meson", "setup", "build"]) - def clean(self, session, resolver, fixers): - self._setup(session, fixers) - run_with_build_fixers(session, ["ninja", "-C", "build", "clean"], fixers) + def clean(self, session, resolver): + self._setup(session) + run_detecting_problems(session, ["ninja", "-C", "build", "clean"]) - def build(self, session, resolver, fixers): - self._setup(session, fixers) - run_with_build_fixers(session, ["ninja", "-C", "build"], fixers) + def build(self, session, resolver): + self._setup(session) + run_detecting_problems(session, ["ninja", "-C", "build"]) - def dist(self, session, resolver, fixers, target_directory, quiet=False): - self._setup(session, fixers) + def dist(self, session, resolver, target_directory, quiet=False): + self._setup(session) with DistCatcher([session.external_path("build/meson-dist")]) as dc: try: - run_with_build_fixers(session, ["ninja", "-C", "build", "dist"], fixers) + run_detecting_problems( + session, ["ninja", "-C", "build", "dist"]) except UnidentifiedError as e: - if "ninja: error: unknown target 'dist', did you mean 'dino'?" in e.lines: + if ("ninja: error: unknown target 'dist', did you mean 'dino'?" + in e.lines): raise NotImplementedError raise return dc.copy_single(target_directory) - def test(self, session, resolver, fixers): - self._setup(session, fixers) - run_with_build_fixers(session, ["ninja", "-C", "build", "test"], fixers) + def test(self, session, resolver): + self._setup(session) + run_detecting_problems(session, ["ninja", "-C", "build", "test"]) - def install(self, session, resolver, fixers, install_target): - self._setup(session, fixers) - run_with_build_fixers(session, ["ninja", "-C", "build", "install"], fixers) + def install(self, session, resolver, install_target): + self._setup(session) + run_detecting_problems(session, ["ninja", "-C", "build", "install"]) @classmethod def probe(cls, path): @@ -823,6 +941,40 @@ class Meson(BuildSystem): logging.debug("Found meson.build, assuming meson package.") return Meson(os.path.join(path, "meson.build")) + def _introspect(self, session, fixers, args): + ret = run_with_build_fixers( + fixers, + session, + ["meson", "introspect"] + args + ['./meson.build']) + import json + return json.loads(''.join(ret)) + + def get_declared_dependencies(self, session, fixers=None): + resp = self._introspect(session, fixers, ["--scan-dependencies"]) + for entry in resp: + version = entry.get('version', []) + minimum_version = None + if len(version) == 1 and version[0].startswith('>='): + minimum_version = version[0][2:] + elif len(version) > 1: + logging.warning( + 'Unable to parse version constraints: %r', version) + # TODO(jelmer): Include entry['required'] + yield ( + "core", + VagueDependencyRequirement( + entry['name'], minimum_version=minimum_version)) + + def get_declared_outputs(self, session, fixers=None): + resp = self._introspect(session, fixers, ["--targets"]) + for entry in resp: + if not entry['installed']: + continue + if entry['type'] == 'executable': + for name in entry['filename']: + yield BinaryOutput(os.path.basename(name)) + # TODO(jelmer): Handle other types + class Npm(BuildSystem): @@ -840,49 +992,49 @@ class Npm(BuildSystem): return "%s(%r)" % (type(self).__name__, self.path) def get_declared_dependencies(self, session, fixers=None): - if "dependencies" in self.package: - for name, unused_version in self.package["dependencies"].items(): - # TODO(jelmer): Look at version - yield "core", NodePackageRequirement(name) - if "devDependencies" in self.package: - for name, unused_version in self.package["devDependencies"].items(): - # TODO(jelmer): Look at version - yield "build", NodePackageRequirement(name) + for name, unused_version in self.package.get( + "dependencies", {}).items(): + # TODO(jelmer): Look at version + yield "core", NodePackageRequirement(name) + for name, unused_version in self.package.get( + "devDependencies", {}).items(): + # TODO(jelmer): Look at version + yield "build", NodePackageRequirement(name) def setup(self, session, resolver): binary_req = BinaryRequirement("npm") if not binary_req.met(session): resolver.install([binary_req]) - def dist(self, session, resolver, fixers, target_directory, quiet=False): + def dist(self, session, resolver, target_directory, quiet=False): self.setup(session, resolver) with DistCatcher([session.external_path(".")]) as dc: - run_with_build_fixers(session, ["npm", "pack"], fixers) + run_detecting_problems(session, ["npm", "pack"]) return dc.copy_single(target_directory) - def test(self, session, resolver, fixers): + def test(self, session, resolver): self.setup(session, resolver) - test_script = self.package["scripts"].get("test") + test_script = self.package.get("scripts", {}).get("test") if test_script: - run_with_build_fixers(session, shlex.split(test_script), fixers) + run_detecting_problems(session, ['bash', '-c', test_script]) else: - raise NotImplementedError + logging.info('No test command defined in package.json') - def build(self, session, resolver, fixers): + def build(self, session, resolver): self.setup(session, resolver) - build_script = self.package["scripts"].get("build") + build_script = self.package.get("scripts", {}).get("build") if build_script: - run_with_build_fixers(session, shlex.split(build_script), fixers) + run_detecting_problems(session, ['bash', '-c', build_script]) else: - raise NotImplementedError + logging.info('No build command defined in package.json') - def clean(self, session, resolver, fixers): + def clean(self, session, resolver): self.setup(session, resolver) - clean_script = self.package["scripts"].get("clean") + clean_script = self.package.get("scripts", {}).get("clean") if clean_script: - run_with_build_fixers(session, shlex.split(clean_script), fixers) + run_detecting_problems(session, ['bash', '-c', clean_script]) else: - raise NotImplementedError + logging.info('No clean command defined in package.json') @classmethod def probe(cls, path): @@ -898,20 +1050,31 @@ class Waf(BuildSystem): def __init__(self, path): self.path = path - def setup(self, session, resolver, fixers): + def setup(self, session, resolver): binary_req = BinaryRequirement("python3") if not binary_req.met(session): resolver.install([binary_req]) - def dist(self, session, resolver, fixers, target_directory, quiet=False): - self.setup(session, resolver, fixers) + def build(self, session, resolver): + try: + run_detecting_problems(session, [self.path, 'build']) + except UnidentifiedError as e: + if ("The project was not configured: run \"waf configure\" first!" + in e.lines): + run_detecting_problems(session, [self.path, 'configure']) + run_detecting_problems(session, [self.path, 'build']) + else: + raise + + def dist(self, session, resolver, target_directory, quiet=False): + self.setup(session, resolver) with DistCatcher.default(session.external_path(".")) as dc: - run_with_build_fixers(session, ["./waf", "dist"], fixers) + run_detecting_problems(session, ["./waf", "dist"]) return dc.copy_single(target_directory) - def test(self, session, resolver, fixers): - self.setup(session, resolver, fixers) - run_with_build_fixers(session, ["./waf", "test"], fixers) + def test(self, session, resolver): + self.setup(session, resolver) + run_detecting_problems(session, ["./waf", "test"]) @classmethod def probe(cls, path): @@ -927,22 +1090,25 @@ class Gem(BuildSystem): def __init__(self, path): self.path = path - def dist(self, session, resolver, fixers, target_directory, quiet=False): + def dist(self, session, resolver, target_directory, quiet=False): gemfiles = [ - entry.name for entry in session.scandir(".") if entry.name.endswith(".gem") + entry.name for entry in session.scandir(".") + if entry.name.endswith(".gem") ] if len(gemfiles) > 1: logging.warning("More than one gemfile. Trying the first?") with DistCatcher.default(session.external_path(".")) as dc: - run_with_build_fixers( + run_detecting_problems( session, - [guaranteed_which(session, resolver, "gem2tgz"), gemfiles[0]], fixers) + [guaranteed_which(session, resolver, "gem2tgz"), + gemfiles[0]]) return dc.copy_single(target_directory) @classmethod def probe(cls, path): gemfiles = [ - entry.path for entry in os.scandir(path) if entry.name.endswith(".gem") + entry.path for entry in os.scandir(path) + if entry.name.endswith(".gem") ] if gemfiles: return cls(gemfiles[0]) @@ -963,9 +1129,11 @@ class DistZilla(BuildSystem): (key, value) = line[2:].strip().split(b"=", 1) except ValueError: continue - if key.strip() == b"class" and value.strip().startswith(b"'Dist::Inkt"): + if (key.strip() == b"class" + and value.strip().startswith(b"'Dist::Inkt")): logging.debug( - "Found Dist::Inkt section in dist.ini, " "assuming distinkt." + "Found Dist::Inkt section in dist.ini, " + "assuming distinkt." ) self.name = "dist-inkt" self.dist_inkt_class = value.decode().strip("'") @@ -979,39 +1147,51 @@ class DistZilla(BuildSystem): ] ) - def dist(self, session, resolver, fixers, target_directory, quiet=False): + def dist(self, session, resolver, target_directory, quiet=False): self.setup(resolver) if self.name == "dist-inkt": with DistCatcher.default(session.external_path(".")) as dc: - run_with_build_fixers(session, [guaranteed_which(session, resolver, "distinkt-dist")], fixers) + run_detecting_problems( + session, + [guaranteed_which(session, resolver, "distinkt-dist")]) return dc.copy_single(target_directory) else: # Default to invoking Dist::Zilla with DistCatcher.default(session.external_path(".")) as dc: - run_with_build_fixers(session, [guaranteed_which(session, resolver, "dzil"), "build", "--tgz"], fixers) + run_detecting_problems( + session, + [guaranteed_which(session, resolver, "dzil"), + "build", "--tgz"]) return dc.copy_single(target_directory) - def test(self, session, resolver, fixers): + def test(self, session, resolver): + # see also + # https://perlmaven.com/how-to-run-the-tests-of-a-typical-perl-module self.setup(resolver) - run_with_build_fixers(session, [guaranteed_which(session, resolver, "dzil"), "test"], fixers) + run_detecting_problems( + session, + [guaranteed_which(session, resolver, "dzil"), "test"]) - def build(self, session, resolver, fixers): + def build(self, session, resolver): self.setup(resolver) - run_with_build_fixers(session, [guaranteed_which(session, resolver, "dzil"), "build"], fixers) + run_detecting_problems( + session, + [guaranteed_which(session, resolver, "dzil"), "build"]) @classmethod def probe(cls, path): - if os.path.exists(os.path.join(path, "dist.ini")) and not os.path.exists( - os.path.join(path, "Makefile.PL") - ): + if (os.path.exists(os.path.join(path, "dist.ini")) + and not os.path.exists(os.path.join(path, "Makefile.PL"))): return cls(os.path.join(path, "dist.ini")) def get_declared_dependencies(self, session, fixers=None): if os.path.exists(os.path.join(self.path, "dist.ini")): - lines = run_with_build_fixers(session, ["dzil", "authordeps"], fixers) + lines = run_with_build_fixers( + fixers, session, ["dzil", "authordeps"]) for entry in lines: yield "build", PerlModuleRequirement(entry.strip()) - if os.path.exists(os.path.join(os.path.dirname(self.path), "cpanfile")): + if os.path.exists( + os.path.join(os.path.dirname(self.path), "cpanfile")): yield from _declared_deps_from_cpanfile(session, fixers) @@ -1030,22 +1210,24 @@ class RunTests(BuildSystem): if os.path.exists(os.path.join(path, "runtests.sh")): return cls(path) - def test(self, session, resolver, fixers): + def test(self, session, resolver): if shebang_binary(os.path.join(self.path, "runtests.sh")) is not None: - run_with_build_fixers(session, ["./runtests.sh"], fixers) + run_detecting_problems(session, ["./runtests.sh"]) else: - run_with_build_fixers(session, ["/bin/bash", "./runtests.sh"], fixers) + run_detecting_problems(session, ["/bin/bash", "./runtests.sh"]) def _read_cpanfile(session, args, kind, fixers): - for line in run_with_build_fixers(session, ["cpanfile-dump"] + args, fixers): + for line in run_with_build_fixers( + fixers, session, ["cpanfile-dump"] + args): line = line.strip() if line: yield kind, PerlModuleRequirement(line) def _declared_deps_from_cpanfile(session, fixers): - yield from _read_cpanfile(session, ["--configure", "--build"], "build", fixers) + yield from _read_cpanfile( + session, ["--configure", "--build"], "build", fixers) yield from _read_cpanfile(session, ["--test"], "test", fixers) @@ -1069,6 +1251,53 @@ def _declared_deps_from_meta_yml(f): # TODO(jelmer): recommends +class CMake(BuildSystem): + + name = "cmake" + + def __init__(self, path): + self.path = path + self.builddir = 'build' + + def __repr__(self): + return "%s(%r)" % (type(self).__name__, self.path) + + def setup(self, session, resolver): + if not session.exists(self.builddir): + session.mkdir(self.builddir) + try: + run_detecting_problems( + session, ["cmake", '.', '-B%s' % self.builddir]) + except Exception: + session.rmtree(self.builddir) + raise + + @classmethod + def probe(cls, path): + if os.path.exists(os.path.join(path, 'CMakeLists.txt')): + return cls(path) + return None + + def build(self, session, resolver): + self.setup(session, resolver) + run_detecting_problems( + session, ["cmake", "--build", self.builddir]) + + def install(self, session, resolver, install_target): + self.setup(session, resolver) + run_detecting_problems( + session, ["cmake", "--install", self.builddir]) + + def clean(self, session, resolver): + self.setup(session, resolver) + run_detecting_problems( + session, + ["cmake", "--build %s" % self.builddir, ".", "--target", "clean"]) + + def test(self, session, resolver): + raise NotImplementedError(self.test) + + class Make(BuildSystem): def __init__(self, path): @@ -1080,67 +1309,72 @@ class Make(BuildSystem): elif any([os.path.exists(os.path.join(path, n)) for n in ['configure.ac', 'configure.in', 'autogen.sh']]): self.name = 'autoconf' - elif os.path.exists(os.path.join(path, "CMakeLists.txt")): - self.name = 'cmake' + elif any([n.name.endswith(".pro") for n in os.scandir(path)]): + self.name = 'qmake' else: self.name = "make" def __repr__(self): return "%s(%r)" % (type(self).__name__, self.path) - def setup(self, session, resolver, fixers, prefix=None): + def setup(self, session, resolver, prefix=None): def makefile_exists(): return any( - [session.exists(p) for p in ["Makefile", "GNUmakefile", "makefile"]] + [session.exists(p) + for p in ["Makefile", "GNUmakefile", "makefile"]] ) if session.exists("Makefile.PL") and not makefile_exists(): - run_with_build_fixers(session, ["perl", "Makefile.PL"], fixers) + run_detecting_problems(session, ["perl", "Makefile.PL"]) if not makefile_exists() and not session.exists("configure"): if session.exists("autogen.sh"): - if shebang_binary(os.path.join(self.path, "autogen.sh")) is None: - run_with_build_fixers(session, ["/bin/sh", "./autogen.sh"], fixers) + if shebang_binary( + os.path.join(self.path, "autogen.sh")) is None: + run_detecting_problems( + session, ["/bin/sh", "./autogen.sh"]) try: - run_with_build_fixers(session, ["./autogen.sh"], fixers) + run_detecting_problems(session, ["./autogen.sh"]) except UnidentifiedError as e: if ( "Gnulib not yet bootstrapped; " "run ./bootstrap instead." in e.lines ): - run_with_build_fixers(session, ["./bootstrap"], fixers) - run_with_build_fixers(session, ["./autogen.sh"], fixers) + run_detecting_problems(session, ["./bootstrap"]) + run_detecting_problems(session, ["./autogen.sh"]) else: raise - elif session.exists("configure.ac") or session.exists("configure.in"): - run_with_build_fixers(session, ["autoreconf", "-i"], fixers) + elif (session.exists("configure.ac") + or session.exists("configure.in")): + run_detecting_problems(session, ["autoreconf", "-i"]) if not makefile_exists() and session.exists("configure"): extra_args = [] if prefix is not None: extra_args.append('--prefix=%s' % prefix) - run_with_build_fixers(session, ["./configure"] + extra_args, fixers) + run_detecting_problems(session, ["./configure"] + extra_args) if not makefile_exists() and any( [n.name.endswith(".pro") for n in session.scandir(".")] ): - run_with_build_fixers(session, ["qmake"], fixers) + run_detecting_problems(session, ["qmake"]) - if not makefile_exists() and session.exists('CMakeLists.txt'): - if not session.exists("build"): - session.mkdir('build') - run_with_build_fixers(session, ["cmake", '..'], fixers, cwd='build') + def build(self, session, resolver): + self.setup(session, resolver) + if self.name == 'qmake': + default_target = None + else: + default_target = 'all' + self._run_make( + session, + [default_target] if default_target else []) - def build(self, session, resolver, fixers): - self.setup(session, resolver, fixers) - self._run_make(session, ["all"], fixers) + def clean(self, session, resolver): + self.setup(session, resolver) + self._run_make(session, ["clean"]) - def clean(self, session, resolver, fixers): - self.setup(session, resolver, fixers) - self._run_make(session, ["clean"], fixers) - - def _run_make(self, session, args, fixers, prefix=None): + def _run_make(self, session, args, prefix=None): def _wants_configure(line): if line.startswith("Run ./configure"): return True @@ -1150,96 +1384,116 @@ class Make(BuildSystem): return True if line.startswith("The project was not configured"): return True + if re.match( + r'Makefile:[0-9]+: \*\*\* ' + r'You need to run \.\/configure .*', line): + return True return False - if session.exists('build'): + if session.exists('build/Makefile'): cwd = 'build' else: cwd = None try: - run_with_build_fixers(session, ["make"] + args, fixers, cwd=cwd) + run_detecting_problems(session, ["make"] + args, cwd=cwd) except UnidentifiedError as e: - if len(e.lines) < 5 and any([_wants_configure(line) for line in e.lines]): + if len(e.lines) < 5 and any( + [_wants_configure(line) for line in e.lines]): extra_args = [] if prefix is not None: extra_args.append("--prefix=%s" % prefix) - run_with_build_fixers(session, ["./configure"] + extra_args, fixers) - run_with_build_fixers(session, ["make"] + args, fixers) + run_detecting_problems(session, ["./configure"] + extra_args) + run_detecting_problems(session, ["make"] + args) elif ( "Reconfigure the source tree " "(via './config' or 'perl Configure'), please." ) in e.lines: - run_with_build_fixers(session, ["./config"], fixers) - run_with_build_fixers(session, ["make"] + args, fixers) + run_detecting_problems(session, ["./config"]) + run_detecting_problems(session, ["make"] + args) else: raise - def test(self, session, resolver, fixers): - self.setup(session, resolver, fixers) - self._run_make(session, ["check"], fixers) + def test(self, session, resolver): + self.setup(session, resolver) + for target in ["check", "test"]: + try: + self._run_make(session, [target]) + except UnidentifiedError as e: + if (("make: *** No rule to make target '%s'. Stop." % target) + in e.lines): + pass + else: + raise + else: + break + else: + if os.path.isdir('t'): + # See + # https://perlmaven.com/how-to-run-the-tests-of-a-typical-perl-module + run_detecting_problems(session, ["prove", "-b", "t/"]) + else: + logging.warning('No test target found') - def install(self, session, resolver, fixers, install_target): - self.setup(session, resolver, fixers, prefix=install_target.prefix) - self._run_make(session, ["install"], fixers, prefix=install_target.prefix) + def install(self, session, resolver, install_target): + self.setup(session, resolver, prefix=install_target.prefix) + self._run_make(session, ["install"], prefix=install_target.prefix) - def dist(self, session, resolver, fixers, target_directory, quiet=False): - self.setup(session, resolver, fixers) + def dist(self, session, resolver, target_directory, quiet=False): + self.setup(session, resolver) with DistCatcher.default(session.external_path(".")) as dc: try: - self._run_make(session, ["dist"], fixers) + self._run_make(session, ["dist"]) except UnidentifiedError as e: - if "make: *** No rule to make target 'dist'. Stop." in e.lines: + if ("make: *** No rule to make target 'dist'. Stop." + in e.lines): raise NotImplementedError - elif "make[1]: *** No rule to make target 'dist'. Stop." in e.lines: + elif ("make[1]: *** No rule to make target 'dist'. Stop." + in e.lines): raise NotImplementedError - elif "ninja: error: unknown target 'dist', did you mean 'dino'?" in e.lines: + elif ("ninja: error: unknown target 'dist', " + "did you mean 'dino'?" in e.lines): raise NotImplementedError elif ( "Please try running 'make manifest' and then run " "'make dist' again." in e.lines ): - run_with_build_fixers(session, ["make", "manifest"], fixers) - run_with_build_fixers(session, ["make", "dist"], fixers) + run_detecting_problems(session, ["make", "manifest"]) + run_detecting_problems(session, ["make", "dist"]) elif any( [ re.match( r"(Makefile|GNUmakefile|makefile):[0-9]+: " r"\*\*\* Missing \'Make.inc\' " - r"Run \'./configure \[options\]\' and retry. Stop.", - line, + r"Run \'./configure \[options\]\' and retry. " + r"Stop.", line, ) for line in e.lines ] ): - run_with_build_fixers(session, ["./configure"], fixers) - run_with_build_fixers(session, ["make", "dist"], fixers) + run_detecting_problems(session, ["./configure"]) + run_detecting_problems(session, ["make", "dist"]) elif any( [ re.match( - r"Problem opening MANIFEST: No such file or directory " - r"at .* line [0-9]+\.", - line, + r"Problem opening MANIFEST: " + r"No such file or directory " + r"at .* line [0-9]+\.", line, ) for line in e.lines ] ): - run_with_build_fixers(session, ["make", "manifest"], fixers) - run_with_build_fixers(session, ["make", "dist"], fixers) + run_detecting_problems(session, ["make", "manifest"]) + run_detecting_problems(session, ["make", "dist"]) else: raise return dc.copy_single(target_directory) def get_declared_dependencies(self, session, fixers=None): - something = False # TODO(jelmer): Split out the perl-specific stuff? if os.path.exists(os.path.join(self.path, "META.yml")): with open(os.path.join(self.path, "META.yml"), "rb") as f: yield from _declared_deps_from_meta_yml(f) - something = True if os.path.exists(os.path.join(self.path, "cpanfile")): yield from _declared_deps_from_cpanfile(session, fixers) - something = True - if not something: - raise NotImplementedError @classmethod def probe(cls, path): @@ -1251,7 +1505,6 @@ class Make(BuildSystem): "GNUmakefile", "makefile", "Makefile.PL", - "CMakeLists.txt", "autogen.sh", "configure.ac", "configure.in", @@ -1280,6 +1533,9 @@ class Cargo(BuildSystem): with open(path, "r") as f: self.cargo = load(f) + def install_declared_requirements(self, stages, session, resolver, fixers): + run_with_build_fixers(fixers, session, ["cargo", "fetch"]) + def get_declared_dependencies(self, session, fixers=None): if "dependencies" in self.cargo: for name, details in self.cargo["dependencies"].items(): @@ -1292,14 +1548,26 @@ class Cargo(BuildSystem): version=details.get("version"), ) - def test(self, session, resolver, fixers): - run_with_build_fixers(session, ["cargo", "test"], fixers) + def test(self, session, resolver): + run_detecting_problems(session, ["cargo", "test"]) - def clean(self, session, resolver, fixers): - run_with_build_fixers(session, ["cargo", "clean"], fixers) + def clean(self, session, resolver): + run_detecting_problems(session, ["cargo", "clean"]) - def build(self, session, resolver, fixers): - run_with_build_fixers(session, ["cargo", "build"], fixers) + def build(self, session, resolver): + try: + run_detecting_problems(session, ["cargo", "generate"]) + except UnidentifiedError as e: + if e.lines != ['error: no such subcommand: `generate`']: + raise + run_detecting_problems(session, ["cargo", "build"]) + + def install(self, session, resolver, install_target): + args = [] + if install_target.prefix: + args.append('-root=%s' % install_target.prefix) + run_detecting_problems( + session, ["cargo", "install", "--path=."] + args) @classmethod def probe(cls, path): @@ -1344,16 +1612,16 @@ class Golang(BuildSystem): def __repr__(self): return "%s()" % (type(self).__name__) - def test(self, session, resolver, fixers): - run_with_build_fixers(session, ["go", "test", "./..."], fixers) + def test(self, session, resolver): + run_detecting_problems(session, ["go", "test", "./..."]) - def build(self, session, resolver, fixers): - run_with_build_fixers(session, ["go", "build"], fixers) + def build(self, session, resolver): + run_detecting_problems(session, ["go", "build"]) - def install(self, session, resolver, fixers): - run_with_build_fixers(session, ["go", "install"], fixers) + def install(self, session, resolver, install_target): + run_detecting_problems(session, ["go", "install"]) - def clean(self, session, resolver, fixers): + def clean(self, session, resolver): session.check_call(["go", "clean"]) def get_declared_dependencies(self, session, fixers=None): @@ -1365,7 +1633,8 @@ class Golang(BuildSystem): yield "build", GoRequirement(parts[1]) elif parts[0] == "require": yield "build", GoPackageRequirement( - parts[1], parts[2].lstrip("v") if len(parts) > 2 else None + parts[1], + parts[2].lstrip("v") if len(parts) > 2 else None ) elif parts[0] == "exclude": pass # TODO(jelmer): Create conflicts? @@ -1374,7 +1643,8 @@ class Golang(BuildSystem): elif parts[0] == "module": pass else: - logging.warning("Unknown directive %s in go.mod", parts[0]) + logging.warning( + "Unknown directive %s in go.mod", parts[0]) @classmethod def probe(cls, path): @@ -1398,25 +1668,28 @@ class Maven(BuildSystem): def __init__(self, path): self.path = path + def __repr__(self): + return "%s(%r)" % (type(self).__name__, self.path) + @classmethod def probe(cls, path): if os.path.exists(os.path.join(path, "pom.xml")): logging.debug("Found pom.xml, assuming maven package.") return cls(os.path.join(path, "pom.xml")) - def test(self, session, resolver, fixers): - run_with_build_fixers(session, ["mvn", "test"], fixers) + def test(self, session, resolver): + run_detecting_problems(session, ["mvn", "test"]) - def clean(self, session, resolver, fixers): - run_with_build_fixers(session, ["mvn", "clean"], fixers) + def clean(self, session, resolver): + run_detecting_problems(session, ["mvn", "clean"]) - def install(self, session, resolver, fixers, install_target): - run_with_build_fixers(session, ["mvn", "install"], fixers) + def install(self, session, resolver, install_target): + run_detecting_problems(session, ["mvn", "install"]) - def build(self, session, resolver, fixers): - run_with_build_fixers(session, ["mvn", "compile"], fixers) + def build(self, session, resolver): + run_detecting_problems(session, ["mvn", "compile"]) - def dist(self, session, resolver, fixers, target_directory, quiet=False): + def dist(self, session, resolver, target_directory, quiet=False): # TODO(jelmer): 'mvn generate-sources' creates a jar in target/. # is that what we need? raise NotImplementedError @@ -1453,31 +1726,30 @@ class Cabal(BuildSystem): def __repr__(self): return "%s(%r)" % (type(self).__name__, self.path) - def _run(self, session, args, fixers): + def _run(self, session, args): try: - run_with_build_fixers(session, ["runhaskell", "Setup.hs"] + args, fixers) + run_detecting_problems( + session, ["runhaskell", "Setup.hs"] + args) except UnidentifiedError as e: if "Run the 'configure' command first." in e.lines: - run_with_build_fixers( - session, ["runhaskell", "Setup.hs", "configure"], fixers - ) - run_with_build_fixers( - session, ["runhaskell", "Setup.hs"] + args, fixers - ) + run_detecting_problems( + session, ["runhaskell", "Setup.hs", "configure"]) + run_detecting_problems( + session, ["runhaskell", "Setup.hs"] + args) else: raise - def test(self, session, resolver, fixers): - self._run(session, ["test"], fixers) + def test(self, session, resolver): + self._run(session, ["test"]) - def dist(self, session, resolver, fixers, target_directory, quiet=False): + def dist(self, session, resolver, target_directory, quiet=False): with DistCatcher( [ session.external_path("dist-newstyle/sdist"), session.external_path("dist"), ] ) as dc: - self._run(session, ["sdist"], fixers) + self._run(session, ["sdist"]) return dc.copy_single(target_directory) @classmethod @@ -1515,63 +1787,70 @@ class PerlBuildTiny(BuildSystem): def __repr__(self): return "%s(%r)" % (type(self).__name__, self.path) - def setup(self, session, fixers): - run_with_build_fixers(session, ["perl", "Build.PL"], fixers) + def setup(self, session, fixers=None): + run_with_build_fixers(fixers, session, ["perl", "Build.PL"]) - def test(self, session, resolver, fixers): - self.setup(session, fixers) + def test(self, session, resolver): + self.setup(session) if self.minilla: - run_with_build_fixers(session, ["minil", "test"], fixers) + run_detecting_problems(session, ["minil", "test"]) else: - run_with_build_fixers(session, ["./Build", "test"], fixers) + run_detecting_problems(session, ["./Build", "test"]) - def build(self, session, resolver, fixers): - self.setup(session, fixers) - run_with_build_fixers(session, ["./Build", "build"], fixers) + def build(self, session, resolver): + self.setup(session) + run_detecting_problems(session, ["./Build", "build"]) - def clean(self, session, resolver, fixers): - self.setup(session, fixers) - run_with_build_fixers(session, ["./Build", "clean"], fixers) + def clean(self, session, resolver): + self.setup(session) + run_detecting_problems(session, ["./Build", "clean"]) - def dist(self, session, resolver, fixers, target_directory, quiet=False): - self.setup(session, fixers) + def dist(self, session, resolver, target_directory, quiet=False): + self.setup(session) with DistCatcher([session.external_path('.')]) as dc: if self.minilla: - # minil seems to return 0 even if it didn't produce a tarball :( - run_with_build_fixers( - session, ["minil", "dist"], fixers, + # minil seems to return 0 even if it didn't produce a tarball + # :( + run_detecting_problems( + session, ["minil", "dist"], check_success=lambda retcode, lines: bool(dc.find_files())) else: try: - run_with_build_fixers(session, ["./Build", "dist"], fixers) + run_detecting_problems(session, ["./Build", "dist"]) except UnidentifiedError as e: - if "Can't find dist packages without a MANIFEST file" in e.lines: - run_with_build_fixers(session, ["./Build", "manifest"], fixers) - run_with_build_fixers(session, ["./Build", "dist"], fixers) + if ("Can't find dist packages without a MANIFEST file" + in e.lines): + run_detecting_problems( + session, ["./Build", "manifest"]) + run_detecting_problems(session, ["./Build", "dist"]) elif "No such action 'dist'" in e.lines: raise NotImplementedError else: raise return dc.copy_single(target_directory) - def install(self, session, resolver, fixers, install_target): - self.setup(session, fixers) + def install(self, session, resolver, install_target): + self.setup(session) if self.minilla: - run_with_build_fixers(session, ["minil", "install"], fixers) + run_detecting_problems(session, ["minil", "install"]) else: - run_with_build_fixers(session, ["./Build", "install"], fixers) + run_detecting_problems(session, ["./Build", "install"]) def get_declared_dependencies(self, session, fixers=None): self.setup(session, fixers) if self.minilla: - pass # Minilla doesn't seem to have a way to just regenerate the metadata :( + # Minilla doesn't seem to have a way to just regenerate the + # metadata :( + pass else: try: - run_with_build_fixers(session, ["./Build", "distmeta"], fixers) + run_with_build_fixers(fixers, session, ["./Build", "distmeta"]) except UnidentifiedError as e: if "No such action 'distmeta'" in e.lines: pass - if "Do not run distmeta. Install Minilla and `minil install` instead." in e.lines: + if ("Do not run distmeta. " + "Install Minilla and `minil install` instead." + in e.lines): self.minilla = True else: raise @@ -1584,11 +1863,12 @@ class PerlBuildTiny(BuildSystem): @classmethod def probe(cls, path): if os.path.exists(os.path.join(path, "Build.PL")): - logging.debug("Found Build.PL, assuming Module::Build::Tiny package.") + logging.debug( + "Found Build.PL, assuming Module::Build::Tiny package.") return cls(path) -BUILDSYSTEM_CLSES = [ +BUILDSYSTEM_CLSES: List[Type[BuildSystem]] = [ Pear, SetupPy, Npm, @@ -1605,6 +1885,8 @@ BUILDSYSTEM_CLSES = [ R, Octave, Bazel, + CMake, + GnomeShellExtension, # Make is intentionally at the end of the list. Make, Composer, @@ -1612,21 +1894,30 @@ BUILDSYSTEM_CLSES = [ ] -def scan_buildsystems(path): +def lookup_buildsystem_cls(name: str) -> Type[BuildSystem]: + for bs in BUILDSYSTEM_CLSES: + if bs.name == name: + return bs + raise KeyError(name) + + +def scan_buildsystems(path: str) -> List[Tuple[str, BuildSystem]]: """Detect build systems.""" ret = [] - ret.extend([(".", bs) for bs in detect_buildsystems(path)]) + ret.extend([(".", bs) for bs in detect_buildsystems(path) if bs]) if not ret: # Nothing found. Try the next level? for entry in os.scandir(path): if entry.is_dir(): - ret.extend([(entry.name, bs) for bs in detect_buildsystems(entry.path)]) + ret.extend([ + (entry.name, bs) + for bs in detect_buildsystems(entry.path)]) return ret -def detect_buildsystems(path): +def detect_buildsystems(path: str) -> Iterable[BuildSystem]: for bs_cls in BUILDSYSTEM_CLSES: bs = bs_cls.probe(path) if bs is not None: diff --git a/ognibuild/clean.py b/ognibuild/clean.py index 6bbb3ee..2f76390 100644 --- a/ognibuild/clean.py +++ b/ognibuild/clean.py @@ -15,16 +15,25 @@ # 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): +def run_clean(session, buildsystems, resolver, fixers, log_manager=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() + for buildsystem in buildsystems: - buildsystem.clean(session, resolver, fixers) + iterate_with_build_fixers( + fixers, log_manager.wrap( + partial(buildsystem.clean, session, resolver))) return raise NoBuildToolsFound() diff --git a/ognibuild/debian/__init__.py b/ognibuild/debian/__init__.py index 23a56a1..cf18419 100644 --- a/ognibuild/debian/__init__.py +++ b/ognibuild/debian/__init__.py @@ -29,7 +29,8 @@ 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: diff --git a/ognibuild/debian/apt.py b/ognibuild/debian/apt.py index 1e704b0..52789e7 100644 --- a/ognibuild/debian/apt.py +++ b/ognibuild/debian/apt.py @@ -16,8 +16,9 @@ # 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 +from typing import List, Optional, Iterable import os from buildlog_consultant.apt import ( @@ -37,7 +38,12 @@ from .file_search import ( def run_apt( session: Session, args: List[str], prefix: Optional[List[str]] = None ) -> None: - """Run apt.""" + """Run apt. + + Raises: + DetailedFailure: When a known error occurs + UnidentifiedError: If an unknown error occurs + """ if prefix is None: prefix = [] args = prefix = ["apt", "-y"] + args @@ -48,7 +54,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] == "": + while lines and lines[-1].rstrip('\n') == "": lines.pop(-1) raise UnidentifiedError(retcode, args, lines, secondary=match) @@ -93,13 +99,18 @@ class AptManager(object): def package_exists(self, package): return package in self.apt_cache - def package_versions(self, package): - return list(self.apt_cache[package].versions) + def package_versions(self, package: str) -> Optional[Iterable[Version]]: + try: + return list(self.apt_cache[package].versions) + except KeyError: + return None - def get_packages_for_paths(self, paths, regex=False, case_insensitive=False): + async def get_packages_for_paths( + self, paths, regex: bool = False, case_insensitive: bool = False): logging.debug("Searching for packages containing %r", paths) - return get_packages_for_paths( - paths, self.searchers(), regex=regex, case_insensitive=case_insensitive + return await get_packages_for_paths( + paths, self.searchers(), regex=regex, + case_insensitive=case_insensitive ) def missing(self, packages): diff --git a/ognibuild/debian/build.py b/ognibuild/debian/build.py index 29d689e..eb655a5 100644 --- a/ognibuild/debian/build.py +++ b/ognibuild/debian/build.py @@ -17,6 +17,7 @@ __all__ = [ "get_build_architecture", + "version_add_suffix", "add_dummy_changelog_entry", "build", "DetailedDebianBuildFailure", @@ -24,20 +25,22 @@ __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 -from debmutate.changelog import get_maintainer +from debian.changelog import Changelog, Version, ChangeBlock +from debmutate.changelog import get_maintainer, ChangelogEditor +from debmutate.reformatting import GeneratedFile 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, @@ -45,10 +48,18 @@ 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): @@ -60,7 +71,8 @@ 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 @@ -75,11 +87,12 @@ class MissingChangesFile(Exception): self.filename = filename -def find_changes_files(path, package, version): - non_epoch_version = version.upstream_version +def find_changes_files(path: str, package: str, version: Version): + non_epoch_version = version.upstream_version or '' 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: @@ -109,15 +122,32 @@ 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=None, - maintainer=None, -): + timestamp: Optional[datetime] = None, + maintainer: Tuple[Optional[str], Optional[str]] = None, + allow_reformatting: bool = True, +) -> Version: """Add a dummy changelog entry to a package. Args: @@ -125,18 +155,10 @@ 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: @@ -145,38 +167,38 @@ def add_dummy_changelog_entry( maintainer = get_maintainer() if timestamp is None: timestamp = datetime.now() - 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 + 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 -def get_latest_changelog_entry(local_tree, subpath=""): +def get_latest_changelog_entry( + local_tree: WorkingTree, subpath: str = "") -> ChangeBlock: 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.package, cl.version + return cl[0] -def build( - local_tree, - outf, - build_command=DEFAULT_BUILDER, - result_dir=None, - distribution=None, - subpath="", - source_date_epoch=None, - extra_repositories=None, -): +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): for repo in extra_repositories or []: build_command += " --extra-repository=" + shlex.quote(repo) args = [ @@ -187,8 +209,34 @@ def build( "--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()) @@ -199,22 +247,25 @@ 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, - build_suite, - output_directory, - build_command, - subpath="", - source_date_epoch=None, - extra_repositories=None + 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 ): - build_log_path = os.path.join(output_directory, "build.log") + build_log_path = os.path.join(output_directory, BUILD_LOG_FILENAME) logging.debug("Writing build log to %s", build_log_path) try: with open(build_log_path, "w") as f: @@ -226,6 +277,8 @@ 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: @@ -247,27 +300,39 @@ 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): - subprocess.check_call(["gbp", "dch", "--ignore-branch"], cwd=path) + try: + subprocess.check_call(["gbp", "dch", "--ignore-branch"], cwd=path) + except FileNotFoundError: + raise GitBuildpackageMissing() def attempt_build( - 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 + 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 ): """Attempt a build, with a custom distribution set. @@ -282,7 +347,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: + if run_gbp_dch and not subpath and hasattr(local_tree.controldir, '_git'): gbp_dch(local_tree.abspath(subpath)) if build_changelog_entry is not None: add_dummy_changelog_entry( @@ -295,5 +360,7 @@ 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, ) diff --git a/ognibuild/debian/build_deps.py b/ognibuild/debian/build_deps.py index 246a058..0241f9f 100644 --- a/ognibuild/debian/build_deps.py +++ b/ognibuild/debian/build_deps.py @@ -18,43 +18,46 @@ """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, rootdir): - self.rootdir = rootdir + def __init__(self, apt): + self.apt = apt self._counts = None def __repr__(self): - return "%s(%r)" % (type(self).__name__, self.rootdir) + return "%s(%r)" % (type(self).__name__, self.apt) @classmethod def from_session(cls, session): - return cls(session.location) + return cls(LocalApt(session.location)) def _count(self): counts = {} - 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 + 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 return counts def __call__(self, reqs): if self._counts is None: - self._counts = self._count() + try: + self._counts = self._count() + except NoAptSources: + logging.warning( + "No 'deb-src' in sources.list, " + "unable to break build-depends") + return None by_count = {} for req in reqs: try: @@ -80,5 +83,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("/") + tie_breaker = BuildDependencyTieBreaker(LocalApt()) print(tie_breaker(reqs)) diff --git a/ognibuild/debian/file_search.py b/ognibuild/debian/file_search.py index 5276835..f9422c7 100644 --- a/ognibuild/debian/file_search.py +++ b/ognibuild/debian/file_search.py @@ -17,12 +17,13 @@ # 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 Iterator, List +from typing import List, AsyncIterator import logging @@ -32,11 +33,15 @@ from ..session import Session class FileSearcher(object): def search_files( - self, path: str, regex: bool = False, case_insensitive: bool = False - ) -> Iterator[str]: + self, path: str, regex: bool = False, + case_insensitive: bool = False) -> AsyncIterator[str]: raise NotImplementedError(self.search_files) +class AptFileAccessError(Exception): + """Apt file access error.""" + + class ContentsFileNotFound(Exception): """The contents file was not found.""" @@ -71,7 +76,8 @@ 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 @@ -118,7 +124,7 @@ def _unwrap(f, ext): def load_direct_url(url): - from urllib.error import HTTPError + from urllib.error import HTTPError, URLError from urllib.request import urlopen, Request for ext in [".xz", ".gz", ""]: @@ -128,7 +134,11 @@ def load_direct_url(url): except HTTPError as e: if e.status == 404: continue - raise + 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)) break else: raise FileNotFoundError(url) @@ -187,7 +197,7 @@ class AptFileFileSearcher(FileSearcher): @classmethod def from_session(cls, session): - logging.info('Using apt-file to search apt contents') + logging.debug('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']) @@ -195,7 +205,7 @@ class AptFileFileSearcher(FileSearcher): session.check_call(['apt-file', 'update'], user='root') return cls(session) - def search_files(self, path, regex=False, case_insensitive=False): + async def search_files(self, path, regex=False, case_insensitive=False): args = [] if regex: args.append('-x') @@ -204,15 +214,17 @@ class AptFileFileSearcher(FileSearcher): if case_insensitive: args.append('-i') args.append(path) - 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 + 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) for line in output.splitlines(False): pkg, path = line.split(b': ') @@ -253,7 +265,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) @@ -277,8 +290,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): @@ -286,13 +299,16 @@ 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 - def search_files(self, path, regex=False, case_insensitive=False): + async 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 @@ -338,9 +354,9 @@ class GeneratedFileSearcher(FileSearcher): (path, pkg) = line.strip().split(None, 1) self._db.append(path, pkg) - def search_files( - self, path: str, regex: bool = False, case_insensitive: bool = False - ) -> Iterator[str]: + async def search_files( + self, path: str, regex: bool = False, + case_insensitive: bool = False): for p, pkg in self._db: if regex: flags = 0 @@ -371,16 +387,17 @@ GENERATED_FILE_SEARCHER = GeneratedFileSearcher( ) -def get_packages_for_paths( +async 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: - for pkg in searcher.search_files( + async for pkg in searcher.search_files( path, regex=regex, case_insensitive=case_insensitive ): if pkg not in candidates: @@ -393,8 +410,10 @@ 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() @@ -403,13 +422,14 @@ def main(argv): else: logging.basicConfig(level=logging.INFO) - main_searcher = get_apt_contents_file_searcher(PlainSession()) - main_searcher.load_local() - searchers = [main_searcher, GENERATED_FILE_SEARCHER] + with PlainSession() as session: + main_searcher = get_apt_contents_file_searcher(session) + searchers = [main_searcher, GENERATED_FILE_SEARCHER] - packages = get_packages_for_paths(args.path, searchers=searchers, regex=args.regex) - for package in packages: - print(package) + packages = asyncio.run(get_packages_for_paths( + args.path, searchers=searchers, regex=args.regex)) + for package in packages: + print(package) if __name__ == "__main__": diff --git a/ognibuild/debian/fix_build.py b/ognibuild/debian/fix_build.py index ffb064d..8e8a76b 100644 --- a/ognibuild/debian/fix_build.py +++ b/ognibuild/debian/fix_build.py @@ -22,10 +22,10 @@ __all__ = [ from functools import partial import logging import os -import re import shutil import sys -from typing import List, Set, Optional, Type +import time +from typing import List, Set, Optional, Type, Tuple from debian.deb822 import ( Deb822, @@ -34,6 +34,8 @@ 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, @@ -50,49 +52,7 @@ from debmutate.reformatting import ( GeneratedFile, ) -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 breezy.workspace import reset_tree from debmutate._rules import ( dh_invoke_add_with, @@ -113,18 +73,21 @@ 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 .build import attempt_build, DEFAULT_BUILDER +from .apt import AptManager +from .build import attempt_build, DEFAULT_BUILDER, BUILD_LOG_FILENAME DEFAULT_MAX_ITERATIONS = 10 @@ -150,7 +113,9 @@ 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(): @@ -214,6 +179,11 @@ 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 @@ -231,16 +201,19 @@ 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; dependency %s was already present.", desc) + logging.info( + "Giving up; build dependency %s was already present.", desc) return False logging.info("Adding build dependency: %s", desc) @@ -272,13 +245,18 @@ 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) - return False - if not updater.changed: + 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; 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), @@ -288,7 +266,8 @@ 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) @@ -312,7 +291,7 @@ def python_tie_breaker(tree, subpath, reqs): return True if pkg.startswith("lib%s-" % python_version): return True - if re.match(r'lib%s\.[0-9]-dev' % python_version, pkg): + if pkg == r'lib%s-dev' % python_version: return True return False @@ -337,7 +316,8 @@ 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): @@ -356,9 +336,8 @@ 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) @@ -433,7 +412,7 @@ def fix_missing_makefile_pl(error, phase, context): return False -def coerce_unacceptable_predicate(error, phase, context): +def debcargo_coerce_unacceptable_prerelease(error, phase, context): from debmutate.debcargo import DebcargoEditor with DebcargoEditor(context.abspath('debian/debcargo.toml')) as editor: editor['allow_prerelease_deps'] = True @@ -461,7 +440,8 @@ 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 @@ -481,32 +461,47 @@ class DependencyBuildFixer(BuildFixer): return self._fn(problem, phase, self.apt_resolver, self.context) -def versioned_package_fixers(session, packaging_context, apt): +def versioned_package_fixers(session, packaging_context, apt: AptManager): 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, coerce_unacceptable_predicate), + SimpleBuildFixer( + packaging_context, MissingPerlFile, fix_missing_makefile_pl), + SimpleBuildFixer( + packaging_context, DebcargoUnacceptablePredicate, + debcargo_coerce_unacceptable_prerelease), + SimpleBuildFixer( + packaging_context, DebcargoUnacceptableComparator, + debcargo_coerce_unacceptable_prerelease), ] -def apt_fixers(apt, packaging_context) -> List[BuildFixer]: +def apt_fixers(apt: AptManager, packaging_context, + dep_server_url: Optional[str] = None) -> 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(apt, apt_tie_breakers) + 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) return [ DependencyBuildFixer( packaging_context, apt, AptFetchFailure, retry_apt_failure @@ -515,38 +510,49 @@ def apt_fixers(apt, packaging_context) -> List[BuildFixer]: ] -def default_fixers(local_tree, subpath, apt, committer=None, update_changelog=None): +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): 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 - ) + return (versioned_package_fixers(apt.session, packaging_context, apt) + + apt_fixers(apt, packaging_context, dep_server_url)) def build_incrementally( - local_tree, - apt, - suffix, - build_suite, - output_directory, - build_command, + local_tree: WorkingTree, + apt: AptManager, + suffix: str, + build_suite: str, + output_directory: str, + build_command: str, build_changelog_entry, - committer=None, - max_iterations=DEFAULT_MAX_ITERATIONS, - subpath="", + committer: Optional[str] = None, + max_iterations: int = DEFAULT_MAX_ITERATIONS, + subpath: str = "", source_date_epoch=None, - update_changelog=True, - extra_repositories=None, - fixers=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, ): - fixed_errors = [] + fixed_errors: List[Tuple[Problem, str]] = [] if fixers is None: fixers = default_fixers( local_tree, subpath, apt, committer=committer, - update_changelog=update_changelog) + update_changelog=update_changelog, + dep_server_url=dep_server_url) 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( @@ -558,7 +564,9 @@ def build_incrementally( build_changelog_entry, subpath=subpath, source_date_epoch=source_date_epoch, - run_gbp_dch=(update_changelog is False), + run_gbp_dch=run_gbp_dch, + apt_repository=apt_repository, + apt_repository_key=apt_repository_key, extra_repositories=extra_repositories, ) except UnidentifiedDebianBuildError: @@ -569,15 +577,19 @@ 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( @@ -588,71 +600,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)) - 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) + rotate_logfile(os.path.join(output_directory, BUILD_LOG_FILENAME)) def main(argv=None): import argparse parser = argparse.ArgumentParser("ognibuild.debian.fix_build") - parser.add_argument( - "--suffix", type=str, help="Suffix to use for test builds.", default="fixbuild1" + modifications = parser.add_argument_group('Modifications') + modifications.add_argument( + "--suffix", type=str, help="Suffix to use for test builds.", + default="fixbuild1" ) - parser.add_argument( + modifications.add_argument( "--suite", type=str, help="Suite to target.", default="unstable" ) - parser.add_argument( - "--output-directory", type=str, help="Output directory.", default=None + modifications.add_argument( + "--committer", type=str, help="Committer string (name and email)", + default=None ) - 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( + modifications.add_argument( "--no-update-changelog", action="store_false", default=None, dest="update_changelog", help="do not update the changelog", ) - 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( + modifications.add_argument( "--update-changelog", action="store_true", dest="update_changelog", help="force updating of the changelog", default=None, ) - parser.add_argument("--schroot", type=str, help="chroot to use.") + 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("--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 @@ -669,6 +681,10 @@ 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: @@ -692,6 +708,7 @@ 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: @@ -701,6 +718,21 @@ 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: diff --git a/ognibuild/debian/udd.py b/ognibuild/debian/udd.py index 0c73818..3789585 100644 --- a/ognibuild/debian/udd.py +++ b/ognibuild/debian/udd.py @@ -35,7 +35,8 @@ 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] @@ -54,7 +55,8 @@ 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] diff --git a/ognibuild/dep_server.py b/ognibuild/dep_server.py new file mode 100644 index 0000000..86ece6f --- /dev/null +++ b/ognibuild/dep_server.py @@ -0,0 +1,126 @@ +#!/usr/bin/python +# Copyright (C) 2022 Jelmer Vernooij +# 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()) diff --git a/ognibuild/dist.py b/ognibuild/dist.py index 8206920..0eeb8f9 100644 --- a/ognibuild/dist.py +++ b/ognibuild/dist.py @@ -18,18 +18,19 @@ __all__ = [ "UnidentifiedError", "DetailedFailure", - "create_dist", + "run_dist", "create_dist_schroot", + "create_dist", + "dist", ] 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 @@ -37,16 +38,78 @@ 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 -def run_dist(session, buildsystems, resolver, fixers, target_directory, quiet=False): +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) + # Some things want to write to the user's home directory, # e.g. pip caches in ~/.cache session.create_home() @@ -54,31 +117,34 @@ def run_dist(session, buildsystems, resolver, fixers, target_directory, quiet=Fa logging.info('Using dependency resolver: %s', resolver) for buildsystem in buildsystems: - filename = buildsystem.dist( - session, resolver, fixers, target_directory, quiet=quiet - ) + filename = iterate_with_build_fixers(fixers, log_manager.wrap( + partial( + buildsystem.dist, session, resolver, target_dir, + 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, - cleanup: bool = False, + log_manager: Optional[LogManager] = None, + version: Optional[str] = None, ) -> Optional[str]: - from .buildsystem import detect_buildsystems - from .buildlog import InstallFixer - from .fix_build import BuildFixer - from .fixers import ( - GitIdentityFixer, - SecretGpgKeyFixer, - UnexpandedAutoconfMacroFixer, - ) + """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 + """ if subdir is None: subdir = "package" try: @@ -90,19 +156,11 @@ def create_dist( raise DetailedFailure(1, ["mkdtemp"], NoSpaceOnDevice()) raise - # 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)] + if log_manager is None: + log_manager = NoLogManager() - 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) + return dist(session, export_directory, reldir, target_dir, + log_manager=log_manager, version=version) def create_dist_schroot( @@ -113,30 +171,35 @@ def create_dist_schroot( packaging_subpath: Optional[str] = None, include_controldir: bool = True, subdir: Optional[str] = None, - cleanup: bool = False, + log_manager: Optional[LogManager] = None, ) -> 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, - cleanup=cleanup, - ) + session, tree, target_dir, + include_controldir=include_controldir, subdir=subdir, + log_manager=log_manager) -if __name__ == "__main__": +def main(argv=None): import argparse import breezy.bzr # noqa: F401 import breezy.git # noqa: F401 from breezy.export import export - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser(argv) parser.add_argument( "--chroot", default="unstable-amd64-sbuild", @@ -157,8 +220,12 @@ if __name__ == "__main__": "--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() @@ -169,6 +236,10 @@ if __name__ == "__main__": 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(): @@ -179,30 +250,47 @@ if __name__ == "__main__": packaging_tree = None subdir = None - 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.") + if args.mode == 'vcs': export(tree, "dist.tar.gz", "tgz", None) - 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) + 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:])) diff --git a/ognibuild/dist_catcher.py b/ognibuild/dist_catcher.py index b2546a1..646d819 100644 --- a/ognibuild/dist_catcher.py +++ b/ognibuild/dist_catcher.py @@ -54,7 +54,8 @@ 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): @@ -87,19 +88,23 @@ 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 diff --git a/ognibuild/fix_build.py b/ognibuild/fix_build.py index 4863375..527273c 100644 --- a/ognibuild/fix_build.py +++ b/ognibuild/fix_build.py @@ -17,7 +17,7 @@ from functools import partial import logging -from typing import List, Tuple, Callable, Any, Optional +from typing import List, Tuple, Callable, Optional, TypeVar from buildlog_consultant import Problem from buildlog_consultant.common import ( @@ -29,6 +29,14 @@ 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.""" @@ -44,7 +52,11 @@ class BuildFixer(object): return self._fix(problem, phase) -def run_detecting_problems(session: Session, args: List[str], check_success=None, **kwargs): +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) if check_success is None: def check_success(retcode, contents): return (retcode == 0) @@ -63,17 +75,26 @@ def run_detecting_problems(session: Session, args: List[str], check_success=None 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) -def iterate_with_build_fixers(fixers: List[BuildFixer], cb: Callable[[], Any]): +T = TypeVar('T') + + +def iterate_with_build_fixers( + fixers: List[BuildFixer], + cb: Callable[[], T], limit=DEFAULT_LIMIT) -> T: """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 = [] @@ -86,9 +107,13 @@ def iterate_with_build_fixers(fixers: List[BuildFixer], cb: Callable[[], Any]): 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: @@ -100,23 +125,25 @@ def iterate_with_build_fixers(fixers: List[BuildFixer], cb: Callable[[], Any]): 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( - session: Session, args: List[str], fixers: Optional[List[BuildFixer]], **kwargs -): + fixers: Optional[List[BuildFixer]], session: Session, args: List[str], + quiet: bool = False, **kwargs +) -> List[str]: if fixers is None: fixers = [] return iterate_with_build_fixers( - fixers, partial(run_detecting_problems, session, args, **kwargs) - ) + fixers, + partial(run_detecting_problems, session, args, quiet=quiet, **kwargs)) -def resolve_error(error, phase, fixers): +def resolve_error(error, phase, fixers) -> bool: relevant_fixers = [] for fixer in fixers: if fixer.can_fix(error): diff --git a/ognibuild/fixers.py b/ognibuild/fixers.py index c413f20..b3337db 100644 --- a/ognibuild/fixers.py +++ b/ognibuild/fixers.py @@ -21,8 +21,10 @@ 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 @@ -30,6 +32,18 @@ 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 @@ -77,6 +91,26 @@ 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 diff --git a/ognibuild/info.py b/ognibuild/info.py index c0bc425..ae349bc 100644 --- a/ognibuild/info.py +++ b/ognibuild/info.py @@ -21,11 +21,13 @@ 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:") @@ -35,9 +37,11 @@ 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:") diff --git a/ognibuild/install.py b/ognibuild/install.py index d242ad7..be71846 100644 --- a/ognibuild/install.py +++ b/ognibuild/install.py @@ -15,21 +15,34 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -from .buildsystem import NoBuildToolsFound, InstallTarget +from functools import partial 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): + +def run_install( + session, buildsystems, resolver, fixers, *, user: bool = False, + prefix: Optional[str] = None, log_manager=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: - buildsystem.install(session, resolver, fixers, install_target) + iterate_with_build_fixers( + fixers, + log_manager.wrap( + partial(buildsystem.install, session, resolver, + install_target))) return raise NoBuildToolsFound() diff --git a/ognibuild/logs.py b/ognibuild/logs.py new file mode 100644 index 0000000..dd0d412 --- /dev/null +++ b/ognibuild/logs.py @@ -0,0 +1,105 @@ +#!/usr/bin/python +# Copyright (C) 2018 Jelmer Vernooij +# +# 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 diff --git a/ognibuild/requirements.py b/ognibuild/requirements.py index 29475b9..31e81ec 100644 --- a/ognibuild/requirements.py +++ b/ognibuild/requirements.py @@ -26,16 +26,19 @@ from . import Requirement class PythonPackageRequirement(Requirement): + family = "python-package" + package: str - def __init__(self, package, python_version=None, specs=None, minimum_version=None): - super(PythonPackageRequirement, self).__init__("python-package") + def __init__( + self, package, python_version=None, specs=None, + minimum_version=None): 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): @@ -53,11 +56,29 @@ class PythonPackageRequirement(Requirement): return "python package: %s" % (self.package,) @classmethod - def from_requirement_str(cls, text): + def from_requirement_str(cls, text, python_version=None): from requirements.requirement import Requirement req = Requirement.parse(text) - return cls(package=req.name, specs=req.specs) + 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() def met(self, session): if self.python_version == "cpython3": @@ -74,7 +95,8 @@ 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, ) @@ -82,16 +104,33 @@ 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, @@ -104,6 +143,13 @@ 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__, @@ -114,14 +160,24 @@ 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) @@ -135,14 +191,54 @@ 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 @@ -158,10 +254,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 @@ -169,19 +265,26 @@ 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) - 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() + yield PkgConfigRequirement( + self.name.lower(), minimum_version=self.minimum_version) + try: + from .resolver.apt import AptRequirement + except ModuleNotFoundError: + pass else: - devname = 'lib%s-dev' % self.name.lower() - yield AptRequirement.simple(devname, minimum_version=self.minimum_version) + 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) def met(self, session): for x in self.expand(): @@ -192,19 +295,36 @@ 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 @@ -227,8 +347,9 @@ 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): @@ -242,9 +363,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): @@ -255,41 +376,45 @@ class CargoCrateRequirement(Requirement): crate: str features: Set[str] - version: Optional[str] + api_version: Optional[str] + minimum_version: Optional[str] + family = "cargo-crate" - def __init__(self, crate, features=None, version=None): - super(CargoCrateRequirement, self).__init__("cargo-crate") + def __init__(self, crate, features=None, api_version=None, + minimum_version=None): self.crate = crate if features is None: features = set() self.features = features - self.version = version + self.api_version = api_version + self.minimum_version = minimum_version def __repr__(self): - return "%s(%r, features=%r, version=%r)" % ( + return "%s(%r, features=%r, api_version=%r, minimum_version=%r)" % ( type(self).__name__, self.crate, self.features, - self.version, + self.api_version, + self.minimum_version, ) def __str__(self): + ret = "cargo crate: %s %s" % ( + self.crate, + self.api_version or "") if self.features: - 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 "") + ret += " (%s)" % (", ".join(sorted(self.features))) + if self.minimum_version: + ret += " (>= %s)" % self.minimum_version + return ret 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 @@ -301,9 +426,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): @@ -313,9 +438,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): @@ -323,16 +448,15 @@ class CHeaderRequirement(Requirement): class JavaScriptRuntimeRequirement(Requirement): - def __init__(self): - super(JavaScriptRuntimeRequirement, self).__init__("javascript-runtime") + family = "javascript-runtime" class ValaPackageRequirement(Requirement): package: str + family = "vala" def __init__(self, package: str): - super(ValaPackageRequirement, self).__init__("vala") self.package = package @@ -340,9 +464,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 @@ -351,12 +475,16 @@ 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) @@ -366,9 +494,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): @@ -378,18 +506,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 @@ -397,9 +525,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 @@ -412,7 +540,8 @@ 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,) @@ -432,9 +561,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 @@ -447,7 +576,8 @@ 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,) @@ -466,9 +596,9 @@ class OctavePackageRequirement(Requirement): class LibraryRequirement(Requirement): library: str + family = "lib" def __init__(self, library: str): - super(LibraryRequirement, self).__init__("lib") self.library = library @@ -476,9 +606,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 @@ -486,18 +616,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 @@ -505,9 +635,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 @@ -515,27 +645,29 @@ 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): - super(CMakefileRequirement, self).__init__("cmake-file") + def __init__(self, filename: str, version=None): 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 @@ -551,9 +683,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 @@ -566,6 +698,11 @@ 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(":")) @@ -587,17 +724,16 @@ class MavenArtifactRequirement(Requirement): class GnomeCommonRequirement(Requirement): - def __init__(self): - super(GnomeCommonRequirement, self).__init__("gnome-common") + family = "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 @@ -607,55 +743,70 @@ class JDKFileRequirement(Requirement): class JDKRequirement(Requirement): - def __init__(self): - super(JDKRequirement, self).__init__("jdk") + family = "jdk" class JRERequirement(Requirement): - def __init__(self): - super(JRERequirement, self).__init__("jre") + family = "jre" + + +class QtModuleRequirement(Requirement): + family = "qt-module" + + def __init__(self, module): + self.module = module class QTRequirement(Requirement): - def __init__(self): - super(QTRequirement, self).__init__("qt") + family = "qt" class X11Requirement(Requirement): - def __init__(self): - super(X11Requirement, self).__init__("x11") + family = "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): - def __init__(self): - super(LibtoolRequirement, self).__init__("libtool") + family = "libtool" class IntrospectionTypelibRequirement(Requirement): + family = "introspection-type-lib" + def __init__(self, library): self.library = library @@ -665,9 +816,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 @@ -702,7 +853,25 @@ 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 diff --git a/ognibuild/resolver/__init__.py b/ognibuild/resolver/__init__.py index 7af26d5..958ff2b 100644 --- a/ognibuild/resolver/__init__.py +++ b/ognibuild/resolver/__init__.py @@ -18,8 +18,11 @@ import logging import subprocess -from .. import UnidentifiedError +from typing import Optional, List, Type + +from .. import UnidentifiedError, Requirement from ..fix_build import run_detecting_problems +from ..session import Session class UnsatisfiedRequirements(Exception): @@ -28,13 +31,22 @@ class UnsatisfiedRequirements(Exception): class Resolver(object): - def install(self, requirements): + + name: str + + def __init__(self, session, user_local): + raise NotImplementedError(self.__init__) + + def install(self, requirements: List[Requirement]): raise NotImplementedError(self.install) - def resolve(self, requirement): + def resolve(self, requirement: Requirement) -> Optional[Requirement]: raise NotImplementedError(self.resolve) - def explain(self, requirements): + def resolve_all(self, requirement: Requirement) -> List[Requirement]: + raise NotImplementedError(self.resolve_all) + + def explain(self, requirements: List[Requirement]): raise NotImplementedError(self.explain) def env(self): @@ -42,13 +54,15 @@ 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 "cpan" + return self.name def __repr__(self): return "%s(%r)" % (type(self).__name__, self.session) @@ -109,7 +123,8 @@ 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 @@ -154,7 +169,8 @@ 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 @@ -163,6 +179,7 @@ class TlmgrResolver(Resolver): class CTANResolver(TlmgrResolver): + name = "ctan" def __init__(self, session, user_local=False): super(CTANResolver, self).__init__( @@ -170,13 +187,16 @@ 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 "cran" + return self.name def __repr__(self): return "%s(%r, %r)" % (type(self).__name__, self.session, self.repos) @@ -221,12 +241,14 @@ 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 "octave-forge" + return self.name def __repr__(self): return "%s(%r)" % (type(self).__name__, self.session) @@ -267,6 +289,8 @@ 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 @@ -274,19 +298,25 @@ 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 "hackage" + return self.name def __repr__(self): return "%s(%r)" % (type(self).__name__, self.session) @@ -295,7 +325,8 @@ 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 @@ -329,12 +360,15 @@ 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 "pypi" + return self.name def __repr__(self): return "%s(%r)" % (type(self).__name__, self.session) @@ -380,12 +414,15 @@ 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 "go" + return self.name def __repr__(self): return "%s(%r)" % (type(self).__name__, self.session) @@ -426,17 +463,26 @@ 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 "npm" + return self.name def __repr__(self): return "%s(%r)" % (type(self).__name__, self.session) @@ -472,7 +518,10 @@ class NpmResolver(Resolver): if not isinstance(requirement, NodePackageRequirement): missing.append(requirement) continue - cmd = ["npm", "-g", "install", requirement.package] + cmd = ["npm", "install"] + if not self.user_local: + cmd.append('-g') + cmd.append(requirement.package) logging.info("npm: running %r", cmd) run_detecting_problems(self.session, cmd, user=user) if missing: @@ -529,7 +578,7 @@ class StackedResolver(Resolver): raise UnsatisfiedRequirements(requirements) -NATIVE_RESOLVER_CLS = [ +NATIVE_RESOLVER_CLS: List[Type[Resolver]] = [ CPANResolver, CTANResolver, PypiResolver, @@ -543,24 +592,70 @@ NATIVE_RESOLVER_CLS = [ 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 auto_resolver(session, explain=False): +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): # 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 = [] - # 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]) + 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]) return StackedResolver(resolvers) diff --git a/ognibuild/resolver/apt.py b/ognibuild/resolver/apt.py index 82ef627..ae88a49 100644 --- a/ognibuild/resolver/apt.py +++ b/ognibuild/resolver/apt.py @@ -15,12 +15,13 @@ # 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 from itertools import chain import logging import os import posixpath import re -from typing import Optional, List +from typing import Optional, List, Tuple, Callable, Type, Awaitable from debian.changelog import Version from debian.deb822 import PkgRelation @@ -28,6 +29,7 @@ from debian.deb822 import PkgRelation from ..debian.apt import AptManager from . import Resolver, UnsatisfiedRequirements +from .. import OneOfRequirement from ..requirements import ( Requirement, CargoCrateRequirement, @@ -48,9 +50,11 @@ from ..requirements import ( NodePackageRequirement, LibraryRequirement, BoostComponentRequirement, + KF5ComponentRequirement, StaticLibraryRequirement, RubyFileRequirement, XmlEntityRequirement, + OctavePackageRequirement, SprocketsFileRequirement, JavaClassRequirement, CMakefileRequirement, @@ -61,6 +65,7 @@ from ..requirements import ( JDKRequirement, JRERequirement, QTRequirement, + QtModuleRequirement, X11Requirement, PerlModuleRequirement, PerlFileRequirement, @@ -72,12 +77,17 @@ from ..requirements import ( VagueDependencyRequirement, PerlPreDeclaredRequirement, IntrospectionTypelibRequirement, + PHPExtensionRequirement, + VcsControlDirectoryAccessRequirement, ) class AptRequirement(Requirement): + + family = "apt" + def __init__(self, relations): - super(AptRequirement, self).__init__("apt") + super(AptRequirement, self).__init__() if not isinstance(relations, list): raise TypeError(relations) self.relations = relations @@ -96,17 +106,26 @@ class AptRequirement(Requirement): def pkg_relation_str(self): return PkgRelation.str(self.relations) + def _json(self): + return self.pkg_relation_str() + + @classmethod + def _from_json(cls, text): + return cls.from_str(text) + def __hash__(self): return hash((type(self), self.pkg_relation_str())) def __eq__(self, other): - return isinstance(self, type(other)) and self.relations == other.relations + return (isinstance(self, type(other)) + and self.relations == other.relations) def __str__(self): return "apt requirement: %s" % self.pkg_relation_str() def __repr__(self): - return "%s.from_str(%r)" % (type(self).__name__, self.pkg_relation_str()) + return "%s.from_str(%r)" % ( + type(self).__name__, self.pkg_relation_str()) def package_names(self): for rel in self.relations: @@ -133,32 +152,34 @@ class AptRequirement(Requirement): for rel in self.relations: for entry in rel: - if any(binary_pkg_matches(entry, binary) for binary in binaries): + if any(binary_pkg_matches(entry, binary) + for binary in binaries): break else: return False return True -def resolve_perl_predeclared_req(apt_mgr, req): +async def resolve_perl_predeclared_req(apt_mgr, req): try: req = req.lookup_module() except KeyError: logging.warning( 'Unable to map predeclared function %s to a perl module', req.name) return None - return resolve_perl_module_req(apt_mgr, req) + return await resolve_perl_module_req(apt_mgr, req) -def find_package_names( - apt_mgr: AptManager, paths: List[str], regex: bool = False, case_insensitive=False +async def find_package_names( + apt_mgr: AptManager, paths: List[str], regex: bool = False, + case_insensitive: bool = False ) -> List[str]: if not isinstance(paths, list): raise TypeError(paths) - return apt_mgr.get_packages_for_paths(paths, regex, case_insensitive) + return await apt_mgr.get_packages_for_paths(paths, regex, case_insensitive) -def find_reqs_simple( +async def find_reqs_simple( apt_mgr: AptManager, paths: List[str], regex: bool = False, @@ -169,7 +190,8 @@ def find_reqs_simple( raise TypeError(paths) return [ AptRequirement.simple(package, minimum_version=minimum_version) - for package in find_package_names(apt_mgr, paths, regex, case_insensitive) + for package in await find_package_names( + apt_mgr, paths, regex, case_insensitive) ] @@ -190,8 +212,10 @@ def python_spec_to_apt_rels(pkg_name, specs): next_maj_deb_version = Version(".".join(parts)) deb_version = Version(spec[1]) rels.extend( - [[{"name": pkg_name, "version": (">=", deb_version)}], - [{"name": pkg_name, "version": ("<<", next_maj_deb_version)}]]) + [[{"name": pkg_name, + "version": (">=", deb_version)}], + [{"name": pkg_name, + "version": ("<<", next_maj_deb_version)}]]) elif spec[0] == "!=": deb_version = Version(spec[1]) rels.extend([ @@ -203,28 +227,36 @@ def python_spec_to_apt_rels(pkg_name, specs): n = list(s) n[-1] = str(int(n[-1]) + 1) rels.extend( - [[{"name": pkg_name, "version": (">=", Version(".".join(s)))}], - [{"name": pkg_name, "version": ("<<", Version(".".join(n)))}]]) + [[{"name": pkg_name, + "version": (">=", Version(".".join(s)))}], + [{"name": pkg_name, + "version": ("<<", Version(".".join(n)))}]]) else: - c = {">=": ">=", "<=": "<=", "<": "<<", ">": ">>", "==": "="}[spec[0]] + c = { + ">=": ">=", + "<=": "<=", + "<": "<<", + ">": ">>", + "==": "=", + }[spec[0]] deb_version = Version(spec[1]) rels.append([{"name": pkg_name, "version": (c, deb_version)}]) return rels -def get_package_for_python_package( +async def get_package_for_python_package( apt_mgr, package, python_version: Optional[str], specs=None ): - pypy_regex = "/usr/lib/pypy/dist-packages/%s-.*.egg-info" % re.escape( - package.replace("-", "_") - ) + pypy_regex = ( + "/usr/lib/pypy/dist\\-packages/%s-.*\\.(dist|egg)\\-info" + % re.escape(package.replace("-", "_"))) cpython2_regex = ( - "/usr/lib/python2\\.[0-9]/dist-packages/%s-.*.egg-info" + "/usr/lib/python2\\.[0-9]/dist\\-packages/%s-.*\\.(dist|egg)\\-info" % re.escape(package.replace("-", "_")) ) - cpython3_regex = "/usr/lib/python3/dist-packages/%s-.*.egg-info" % re.escape( - package.replace("-", "_") - ) + cpython3_regex = ( + "/usr/lib/python3/dist\\-packages/%s-.*\\.(dist|egg)\\-info" + % re.escape(package.replace("-", "_"))) if python_version == "pypy": paths = [pypy_regex] elif python_version == "cpython2": @@ -234,109 +266,174 @@ def get_package_for_python_package( elif python_version is None: paths = [cpython3_regex, cpython2_regex, pypy_regex] else: - raise NotImplementedError("unsupported python version %s" % python_version) - names = find_package_names(apt_mgr, paths, regex=True, case_insensitive=True) - return [AptRequirement(python_spec_to_apt_rels(name, specs)) for name in names] + raise NotImplementedError( + "unsupported python version %s" % python_version) + names = await find_package_names( + apt_mgr, paths, regex=True, case_insensitive=True) + return [AptRequirement( + python_spec_to_apt_rels(name, specs)) for name in names] -def get_package_for_python_module(apt_mgr, module, python_version, specs): - cpython3_regexes = [ - posixpath.join( - "/usr/lib/python3/dist-packages", - re.escape(module.replace(".", "/")), - "__init__.py", - ), - posixpath.join( - "/usr/lib/python3/dist-packages", - re.escape(module.replace(".", "/")) + ".py", - ), - posixpath.join( - "/usr/lib/python3\\.[0-9]+/lib-dynload", - re.escape(module.replace(".", "/")) + "\\.cpython-.*\\.so", - ), - posixpath.join( - "/usr/lib/python3\\.[0-9]+/", re.escape(module.replace(".", "/")) + ".py" - ), - posixpath.join( - "/usr/lib/python3\\.[0-9]+/", - re.escape(module.replace(".", "/")), - "__init__.py", - ), - ] - cpython2_regexes = [ - posixpath.join( - "/usr/lib/python2\\.[0-9]/dist-packages", - re.escape(module.replace(".", "/")), - "__init__.py", - ), - posixpath.join( - "/usr/lib/python2\\.[0-9]/dist-packages", - re.escape(module.replace(".", "/")) + ".py", - ), - posixpath.join( - "/usr/lib/python2.\\.[0-9]/lib-dynload", - re.escape(module.replace(".", "/")) + ".so", - ), - ] - pypy_regexes = [ - posixpath.join( - "/usr/lib/pypy/dist-packages", - re.escape(module.replace(".", "/")), - "__init__.py", - ), - posixpath.join( - "/usr/lib/pypy/dist-packages", re.escape(module.replace(".", "/")) + ".py" - ), - posixpath.join( - "/usr/lib/pypy/dist-packages", - re.escape(module.replace(".", "/")) + "\\.pypy-.*\\.so", - ), - ] +def get_possible_python3_paths_for_python_object(object_path): + cpython3_regexes = [] + while True: + cpython3_regexes.extend([ + posixpath.join( + re.escape("/usr/lib/python3/dist-packages"), + re.escape(object_path.replace(".", "/")), + re.escape("__init__.py"), + ), + posixpath.join( + re.escape("/usr/lib/python3/dist-packages"), + re.escape(object_path.replace(".", "/")) + re.escape(".py"), + ), + posixpath.join( + "/usr/lib/python3\\.[0-9]+/lib\\-dynload", + re.escape(object_path.replace(".", "/")) + + ".cpython\\-.*\\.so", + ), + posixpath.join( + "/usr/lib/python3\\.[0-9]+/", + re.escape(object_path.replace(".", "/")) + "\\.py" + ), + posixpath.join( + "/usr/lib/python3\\.[0-9]+/", + re.escape(object_path.replace(".", "/")), + "__init__\\.py", + ), + ]) + try: + object_path, discarded = object_path.rsplit('.', 1) + except ValueError: + break + return cpython3_regexes + + +def get_possible_pypy_paths_for_python_object(object_path): + pypy_regexes = [] + while True: + pypy_regexes.extend([ + posixpath.join( + "/usr/lib/pypy/dist\\-packages", + re.escape(object_path.replace(".", "/")), + "__init__\\.py", + ), + posixpath.join( + "/usr/lib/pypy/dist\\-packages", + re.escape(object_path.replace(".", "/")) + "\\.py" + ), + posixpath.join( + "/usr/lib/pypy/dist\\-packages", + re.escape(object_path.replace(".", "/")) + "\\.pypy-.*\\.so", + ), + ]) + try: + object_path, discarded = object_path.rsplit('.', 1) + except ValueError: + break + return pypy_regexes + + +def get_possible_python2_paths_for_python_object(object_path): + cpython2_regexes = [] + while True: + cpython2_regexes.extend([ + posixpath.join( + "/usr/lib/python2\\.[0-9]/dist\\-packages", + re.escape(object_path.replace(".", "/")), + "__init__\\.py", + ), + posixpath.join( + "/usr/lib/python2\\.[0-9]/dist\\-packages", + re.escape(object_path.replace(".", "/")) + "\\.py", + ), + posixpath.join( + "/usr/lib/python2.\\.[0-9]/lib\\-dynload", + re.escape(object_path.replace(".", "/")) + "\\.so", + ), + ]) + try: + object_path, discarded = object_path.rsplit('.', 1) + except ValueError: + break + return cpython2_regexes + + +async def get_package_for_python_object_path( + apt_mgr, object_path, python_version, specs): + # Try to find the most specific file if python_version == "cpython3": - paths = cpython3_regexes + paths = get_possible_python3_paths_for_python_object(object_path) elif python_version == "cpython2": - paths = cpython2_regexes + paths = get_possible_python2_paths_for_python_object(object_path) elif python_version == "pypy": - paths = pypy_regexes + paths = get_possible_pypy_paths_for_python_object(object_path) elif python_version is None: - paths = cpython3_regexes + cpython2_regexes + pypy_regexes + paths = (get_possible_python3_paths_for_python_object(object_path) + + get_possible_python2_paths_for_python_object(object_path) + + get_possible_pypy_paths_for_python_object(object_path)) else: raise AssertionError("unknown python version %r" % python_version) - names = find_package_names(apt_mgr, paths, regex=True) - return [AptRequirement(python_spec_to_apt_rels(name, specs)) for name in names] + names = await find_package_names(apt_mgr, paths, regex=True) + return [AptRequirement(python_spec_to_apt_rels(name, specs)) + for name in names] vague_map = { "the Gnu Scientific Library": "libgsl-dev", "the required FreeType library": "libfreetype-dev", "the Boost C++ libraries": "libboost-dev", + "the sndfile library": "libsndfile-dev", # TODO(jelmer): Support resolving virtual packages "PythonLibs": "libpython3-dev", + "PythonInterp": "python3", "ZLIB": "libz3-dev", "Osmium": "libosmium2-dev", "glib": "libglib2.0-dev", + "OpenGL": "libgl-dev", - # TODO(jelmer): For Python, check minimum_version and map to python 2 or python 3 + # TODO(jelmer): For Python, check minimum_version and map to python 2 or + # python 3 "Python": "libpython3-dev", "Lua": "liblua5.4-dev", } -def resolve_vague_dep_req(apt_mgr, req): +async def resolve_vague_dep_req(apt_mgr, req): name = req.name options = [] + if ' or ' in name: + for entry in name.split(' or '): + options.extend(await resolve_vague_dep_req( + apt_mgr, VagueDependencyRequirement(entry))) + if name in vague_map: - options.append(AptRequirement.simple(vague_map[name], minimum_version=req.minimum_version)) + options.append(AptRequirement.simple( + vague_map[name], minimum_version=req.minimum_version)) for x in req.expand(): - options.extend(resolve_requirement_apt(apt_mgr, x)) + options.extend(await resolve_requirement_apt(apt_mgr, x)) + + if name.startswith('GNU '): + options.extend(await resolve_vague_dep_req( + apt_mgr, VagueDependencyRequirement(name[4:]))) + + if name.startswith('py') or name.endswith('py'): + # TODO(jelmer): Try harder to determine whether this is a python + # package + options.append(await resolve_requirement_apt( + apt_mgr, PythonPackageRequirement(name))) + # Try even harder if not options: - options.extend(find_reqs_simple( + options.extend(await find_reqs_simple( apt_mgr, [ - posixpath.join("/usr/lib", ".*", "pkgconfig", re.escape(req.name) + "-.*\\.pc"), - posixpath.join("/usr/lib/pkgconfig", re.escape(req.name) + "-.*\\.pc") + posixpath.join( + "/usr/lib", ".*", "pkgconfig", + re.escape(req.name) + "-.*\\.pc"), + posixpath.join( + "/usr/lib/pkgconfig", re.escape(req.name) + "\\-.*\\.pc") ], regex=True, case_insensitive=True, @@ -346,18 +443,29 @@ def resolve_vague_dep_req(apt_mgr, req): return options -def resolve_binary_req(apt_mgr, req): +async def resolve_php_extension_req(apt_mgr, req): + return [AptRequirement.simple("php-%s" % req.extension)] + + +async def resolve_octave_pkg_req(apt_mgr, req): + return [AptRequirement.simple( + "octave-%s" % req.package, minimum_version=req.minimum_version)] + + +async def resolve_binary_req(apt_mgr, req): if posixpath.isabs(req.binary_name): paths = [req.binary_name] else: paths = [ - posixpath.join(dirname, req.binary_name) for dirname in ["/usr/bin", "/bin"] + posixpath.join(dirname, req.binary_name) + for dirname in ["/usr/bin", "/bin"] ] - return find_reqs_simple(apt_mgr, paths) + # TODO(jelmer): Check for binaries which use alternatives + return await find_reqs_simple(apt_mgr, paths) -def resolve_pkg_config_req(apt_mgr, req): - names = find_package_names( +async def resolve_pkg_config_req(apt_mgr, req): + names = await find_package_names( apt_mgr, [ posixpath.join( @@ -367,7 +475,7 @@ def resolve_pkg_config_req(apt_mgr, req): regex=True, ) if not names: - names = find_package_names( + names = await find_package_names( apt_mgr, [posixpath.join("/usr/lib/pkgconfig", req.module + ".pc")] ) return [ @@ -376,16 +484,16 @@ def resolve_pkg_config_req(apt_mgr, req): ] -def resolve_path_req(apt_mgr, req): - return find_reqs_simple(apt_mgr, [req.path]) +async def resolve_path_req(apt_mgr, req): + return await find_reqs_simple(apt_mgr, [req.path]) -def resolve_c_header_req(apt_mgr, req): - reqs = find_reqs_simple( +async def resolve_c_header_req(apt_mgr, req): + reqs = await find_reqs_simple( apt_mgr, [posixpath.join("/usr/include", req.header)], regex=False ) if not reqs: - reqs = find_reqs_simple( + reqs = await find_reqs_simple( apt_mgr, [posixpath.join("/usr/include", ".*", re.escape(req.header))], regex=True, @@ -393,140 +501,149 @@ def resolve_c_header_req(apt_mgr, req): return reqs -def resolve_js_runtime_req(apt_mgr, req): - return find_reqs_simple(apt_mgr, ["/usr/bin/node", "/usr/bin/duk"]) +async def resolve_js_runtime_req(apt_mgr, req): + return await find_reqs_simple(apt_mgr, ["/usr/bin/node", "/usr/bin/duk"]) -def resolve_vala_package_req(apt_mgr, req): +async def resolve_vala_package_req(apt_mgr, req): path = "/usr/share/vala-[0-9.]+/vapi/%s\\.vapi" % re.escape(req.package) - return find_reqs_simple(apt_mgr, [path], regex=True) + return await find_reqs_simple(apt_mgr, [path], regex=True) -def resolve_ruby_gem_req(apt_mgr, req): +async def resolve_ruby_gem_req(apt_mgr, req): paths = [ posixpath.join( "/usr/share/rubygems-integration/all/" "specifications/%s-.*\\.gemspec" % re.escape(req.gem) ) ] - return find_reqs_simple( + return await find_reqs_simple( apt_mgr, paths, regex=True, minimum_version=req.minimum_version ) -def resolve_go_package_req(apt_mgr, req): - return find_reqs_simple( +async def resolve_go_package_req(apt_mgr, req): + return await find_reqs_simple( apt_mgr, - [posixpath.join("/usr/share/gocode/src", re.escape(req.package), ".*")], + [posixpath.join( + "/usr/share/gocode/src", re.escape(req.package), ".*")], regex=True, ) -def resolve_go_req(apt_mgr, req): - return [AptRequirement.simple("golang-go", minimum_version="2:%s" % req.version)] - - -def resolve_dh_addon_req(apt_mgr, req): - paths = [posixpath.join("/usr/share/perl5", req.path)] - return find_reqs_simple(apt_mgr, paths) - - -def resolve_php_class_req(apt_mgr, req): - path = "/usr/share/php/%s.php" % req.php_class.replace("\\", "/") - return find_reqs_simple(apt_mgr, [path]) - - -def resolve_php_package_req(apt_mgr, req): +async def resolve_go_req(apt_mgr, req): return [ - AptRequirement.simple("php-%s" % req.package, minimum_version=req.min_version) + AptRequirement.simple( + "golang-go", minimum_version="2:%s~" % req.version)] + + +async def resolve_dh_addon_req(apt_mgr, req): + paths = [posixpath.join("/usr/share/perl5", req.path)] + return await find_reqs_simple(apt_mgr, paths) + + +async def resolve_php_class_req(apt_mgr, req): + path = "/usr/share/php/%s.php" % req.php_class.replace("\\", "/") + return await find_reqs_simple(apt_mgr, [path]) + + +async def resolve_php_package_req(apt_mgr, req): + return [ + AptRequirement.simple( + "php-%s" % req.package, minimum_version=req.min_version) ] -def resolve_r_package_req(apt_mgr, req): +async def resolve_r_package_req(apt_mgr, req): paths = [ posixpath.join("/usr/lib/R/site-library", req.package, "DESCRIPTION") ] - return find_reqs_simple(apt_mgr, paths, minimum_version=req.minimum_version) + return await find_reqs_simple( + apt_mgr, paths, minimum_version=req.minimum_version) -def resolve_node_module_req(apt_mgr, req): +async def resolve_node_module_req(apt_mgr, req): paths = [ - "/usr/share/nodejs/.*/node_modules/%s/index.js" % re.escape(req.module), - "/usr/lib/nodejs/%s/index.js" % re.escape(req.module), - "/usr/share/nodejs/%s/index.js" % re.escape(req.module), + "/usr/share/nodejs/.*/node_modules/%s/index\\.js" + % re.escape(req.module), + "/usr/lib/nodejs/%s/index\\.js" % re.escape(req.module), + "/usr/share/nodejs/%s/index\\.js" % re.escape(req.module), ] - return find_reqs_simple(apt_mgr, paths, regex=True) + return await find_reqs_simple(apt_mgr, paths, regex=True) -def resolve_node_package_req(apt_mgr, req): +async def resolve_node_package_req(apt_mgr, req): paths = [ - "/usr/share/nodejs/.*/node_modules/%s/package\\.json" % re.escape(req.package), + "/usr/share/nodejs/.*/node_modules/%s/package\\.json" + % re.escape(req.package), "/usr/lib/nodejs/%s/package\\.json" % re.escape(req.package), "/usr/share/nodejs/%s/package\\.json" % re.escape(req.package), ] - return find_reqs_simple(apt_mgr, paths, regex=True) + return await find_reqs_simple(apt_mgr, paths, regex=True) -def resolve_library_req(apt_mgr, req): +async def resolve_library_req(apt_mgr, req): paths = [ - posixpath.join("/usr/lib/lib%s.so$" % re.escape(req.library)), - posixpath.join("/usr/lib/.*/lib%s.so$" % re.escape(req.library)), - posixpath.join("/usr/lib/lib%s.a$" % re.escape(req.library)), - posixpath.join("/usr/lib/.*/lib%s.a$" % re.escape(req.library)), + posixpath.join("/usr/lib/lib%s\\.so$" % re.escape(req.library)), + posixpath.join("/usr/lib/.*/lib%s\\.so$" % re.escape(req.library)), + posixpath.join("/usr/lib/lib%s\\.a$" % re.escape(req.library)), + posixpath.join("/usr/lib/.*/lib%s\\.a$" % re.escape(req.library)), ] - return find_reqs_simple(apt_mgr, paths, regex=True) + return await find_reqs_simple(apt_mgr, paths, regex=True) -def resolve_static_library_req(apt_mgr, req): +async def resolve_static_library_req(apt_mgr, req): paths = [ posixpath.join("/usr/lib/%s$" % re.escape(req.filename)), posixpath.join("/usr/lib/.*/%s$" % re.escape(req.filename)), ] - return find_reqs_simple(apt_mgr, paths, regex=True) + return await find_reqs_simple(apt_mgr, paths, regex=True) -def resolve_ruby_file_req(apt_mgr, req): +async def resolve_ruby_file_req(apt_mgr, req): paths = [posixpath.join("/usr/lib/ruby/vendor_ruby/%s.rb" % req.filename)] - reqs = find_reqs_simple(apt_mgr, paths, regex=False) + reqs = await find_reqs_simple(apt_mgr, paths, regex=False) if reqs: return reqs paths = [ posixpath.join( - r"/usr/share/rubygems-integration/all/gems/([^/]+)/" + r"/usr/share/rubygems\-integration/all/gems/([^/]+)/" "lib/%s\\.rb" % re.escape(req.filename) ) ] - return find_reqs_simple(apt_mgr, paths, regex=True) + return await find_reqs_simple(apt_mgr, paths, regex=True) -def resolve_xml_entity_req(apt_mgr, req): +async def resolve_xml_entity_req(apt_mgr, req): # Ideally we should be using the XML catalog for this, but hardcoding # a few URLs will do for now.. URL_MAP = { - "http://www.oasis-open.org/docbook/xml/": "/usr/share/xml/docbook/schema/dtd/" + "http://www.oasis-open.org/docbook/xml/": + "/usr/share/xml/docbook/schema/dtd/" } for url, path in URL_MAP.items(): if req.url.startswith(url): - search_path = posixpath.join(path, req.url[len(url) :]) + search_path = posixpath.join(path, req.url[len(url):]) break else: return None - return find_reqs_simple(apt_mgr, [search_path], regex=False) + return await find_reqs_simple(apt_mgr, [search_path], regex=False) -def resolve_sprockets_file_req(apt_mgr, req): +async def resolve_sprockets_file_req(apt_mgr, req): if req.content_type == "application/javascript": - path = "/usr/share/.*/app/assets/javascripts/%s\\.js$" % re.escape(req.name) + path = ("/usr/share/.*/app/assets/javascripts/%s\\.js$" + % re.escape(req.name)) else: - logging.warning("unable to handle content type %s", req.content_type) + logging.warning( + "unable to handle content type %s", req.content_type) return None - return find_reqs_simple(apt_mgr, [path], regex=True) + return await find_reqs_simple(apt_mgr, [path], regex=True) -def resolve_java_class_req(apt_mgr, req): - # Unfortunately this only finds classes in jars installed on the host - # system :( +async def resolve_java_class_req(apt_mgr, req): + apt_mgr.satisfy(["java-propose-classpath"]) output = apt_mgr.session.check_output( ["java-propose-classpath", "-c" + req.classname] ) @@ -535,20 +652,22 @@ def resolve_java_class_req(apt_mgr, req): logging.warning("unable to find classpath for %s", req.classname) return False logging.info("Classpath for %s: %r", req.classname, classpath) - return find_reqs_simple(apt_mgr, [classpath]) + return await find_reqs_simple(apt_mgr, [classpath]) -def resolve_cmake_file_req(apt_mgr, req): - paths = ['/usr/lib/.*/cmake/.*/%s' % re.escape(req.filename)] - return find_reqs_simple(apt_mgr, paths, regex=True) +async def resolve_cmake_file_req(apt_mgr, req): + paths = ['/usr/lib/.*/cmake/.*/%s' % re.escape(req.filename), + '/usr/share/.*/cmake/%s' % re.escape(req.filename)] + return await find_reqs_simple(apt_mgr, paths, regex=True) -def resolve_haskell_package_req(apt_mgr, req): - path = "/var/lib/ghc/package\\.conf\\.d/%s-.*\\.conf" % re.escape(req.package) - return find_reqs_simple(apt_mgr, [path], regex=True) +async def resolve_haskell_package_req(apt_mgr, req): + path = ("/var/lib/ghc/package\\.conf\\.d/%s\\-.*\\.conf" + % re.escape(req.package)) + return await find_reqs_simple(apt_mgr, [path], regex=True) -def resolve_maven_artifact_req(apt_mgr, req): +async def resolve_maven_artifact_req(apt_mgr, req): if req.version is None: version = ".*" regex = True @@ -566,50 +685,66 @@ def resolve_maven_artifact_req(apt_mgr, req): escape(req.group_id.replace(".", "/")), escape(req.artifact_id), version, - escape("%s-") + version + escape("." + kind), + escape("%s-" % req.artifact_id) + version + escape("." + kind), ) - return find_reqs_simple(apt_mgr, [path], regex=regex) + return await find_reqs_simple(apt_mgr, [path], regex=regex) -def resolve_gnome_common_req(apt_mgr, req): +async def resolve_gnome_common_req(apt_mgr, req): return [AptRequirement.simple("gnome-common")] -def resolve_jdk_file_req(apt_mgr, req): +async def resolve_jdk_file_req(apt_mgr, req): path = re.escape(req.jdk_path) + ".*/" + re.escape(req.filename) - return find_reqs_simple(apt_mgr, [path], regex=True) + return await find_reqs_simple(apt_mgr, [path], regex=True) -def resolve_jdk_req(apt_mgr, req): +async def resolve_jdk_req(apt_mgr, req): return [AptRequirement.simple("default-jdk")] -def resolve_jre_req(apt_mgr, req): +async def resolve_jre_req(apt_mgr, req): return [AptRequirement.simple("default-jre")] -def resolve_x11_req(apt_mgr, req): +async def resolve_x11_req(apt_mgr, req): return [AptRequirement.simple("libx11-dev")] -def resolve_qt_req(apt_mgr, req): - return find_reqs_simple(apt_mgr, ["/usr/lib/.*/qt[0-9]+/bin/qmake"], regex=True) +async def resolve_qt_req(apt_mgr, req): + return await find_reqs_simple( + apt_mgr, ["/usr/lib/.*/qt[0-9]+/bin/qmake"], regex=True) -def resolve_libtool_req(apt_mgr, req): +async def resolve_qt_module_req(apt_mgr, req): + return await find_reqs_simple( + apt_mgr, + ["/usr/lib/.*/qt5/mkspecs/modules/qt_lib_%s\\.pri" + % re.escape(req.module)], + regex=True) + + +async def resolve_libtool_req(apt_mgr, req): return [AptRequirement.simple("libtool")] -def resolve_perl_module_req(apt_mgr, req): - DEFAULT_PERL_PATHS = ["/usr/share/perl5", "/usr/lib/.*/perl5/.*", "/usr/lib/.*/perl-base"] +async def resolve_perl_module_req(apt_mgr, req): + DEFAULT_PERL_PATHS = [ + "/usr/share/perl5", "/usr/lib/.*/perl5/.*", "/usr/lib/.*/perl-base", + "/usr/lib/.*/perl/[^/]+", "/usr/share/perl/[^/]+"] if req.inc is None: if req.filename is None: - paths = [posixpath.join(inc, re.escape(req.module.replace('::', '/') + '.pm')) for inc in DEFAULT_PERL_PATHS] + paths = [ + posixpath.join( + inc, re.escape(req.module.replace('::', '/') + '.pm')) + for inc in DEFAULT_PERL_PATHS] regex = True elif not posixpath.isabs(req.filename): - paths = [posixpath.join(inc, re.escape(req.filename)) for inc in DEFAULT_PERL_PATHS] + paths = [ + posixpath.join(inc, re.escape(req.filename)) + for inc in DEFAULT_PERL_PATHS] regex = True else: paths = [req.filename] @@ -617,79 +752,92 @@ def resolve_perl_module_req(apt_mgr, req): else: regex = False paths = [posixpath.join(inc, req.filename) for inc in req.inc] - return find_reqs_simple(apt_mgr, paths, regex=regex) + return await find_reqs_simple(apt_mgr, paths, regex=regex) -def resolve_perl_file_req(apt_mgr, req): - return find_reqs_simple(apt_mgr, [req.filename], regex=False) +async def resolve_perl_file_req(apt_mgr, req): + return await find_reqs_simple(apt_mgr, [req.filename], regex=False) -def _find_aclocal_fun(macro): - # TODO(jelmer): Use the API for codesearch.debian.net instead? - defun_prefix = b"AC_DEFUN([%s]," % macro.encode("ascii") - au_alias_prefix = b"AU_ALIAS([%s]," % macro.encode("ascii") - prefixes = [defun_prefix, au_alias_prefix] +def _m4_macro_regex(macro): + defun_prefix = re.escape("AC_DEFUN([%s]," % macro) + au_alias_prefix = re.escape("AU_ALIAS([%s]," % macro) + m4_copy = r"m4_copy\(.*,\s*\[%s\]\)" % re.escape(macro) + return "(" + "|".join([defun_prefix, au_alias_prefix, m4_copy]) + ")" + + +def _find_local_m4_macro(macro): + # TODO(jelmer): Query some external service that can search all binary + # packages? + p = re.compile(_m4_macro_regex(macro).encode('ascii')) for entry in os.scandir("/usr/share/aclocal"): if not entry.is_file(): continue with open(entry.path, "rb") as f: for line in f: - if any([line.startswith(prefix) for prefix in prefixes]): + if any(p.finditer(line)): return entry.path raise KeyError -def resolve_autoconf_macro_req(apt_mgr, req): +async def resolve_autoconf_macro_req(apt_mgr, req): try: - path = _find_aclocal_fun(req.macro) + path = _find_local_m4_macro(req.macro) except KeyError: logging.info("No local m4 file found defining %s", req.macro) return None - return find_reqs_simple(apt_mgr, [path]) + return await find_reqs_simple(apt_mgr, [path]) -def resolve_python_module_req(apt_mgr, req): +async def resolve_python_module_req(apt_mgr, req): if req.minimum_version: specs = [(">=", req.minimum_version)] else: specs = [] if req.python_version == 2: - return get_package_for_python_module(apt_mgr, req.module, "cpython2", specs) + return await get_package_for_python_object_path( + apt_mgr, req.module, "cpython2", specs) elif req.python_version in (None, 3): - return get_package_for_python_module(apt_mgr, req.module, "cpython3", specs) + return await get_package_for_python_object_path( + apt_mgr, req.module, "cpython3", specs) else: return None -def resolve_python_package_req(apt_mgr, req): +async def resolve_python_package_req(apt_mgr, req): if req.python_version == 2: - return get_package_for_python_package( + return await get_package_for_python_package( apt_mgr, req.package, "cpython2", req.specs ) elif req.python_version in (None, 3): - return get_package_for_python_package( + return await get_package_for_python_package( apt_mgr, req.package, "cpython3", req.specs ) else: return None -def resolve_cargo_crate_req(apt_mgr, req): - paths = ["/usr/share/cargo/registry/%s-[0-9]+.*/Cargo.toml" % re.escape(req.crate)] - return find_reqs_simple(apt_mgr, paths, regex=True) +async def resolve_cargo_crate_req(apt_mgr, req): + paths = [ + "/usr/share/cargo/registry/%s\\-[0-9]+.*/Cargo\\.toml" + % re.escape(req.crate)] + return await find_reqs_simple( + apt_mgr, paths, regex=True, minimum_version=req.minimum_version) -def resolve_ca_req(apt_mgr, req): +async def resolve_ca_req(apt_mgr, req): return [AptRequirement.simple("ca-certificates")] -def resolve_introspection_typelib_req(apt_mgr, req): - return find_reqs_simple( - apt_mgr, [r'/usr/lib/.*/girepository-.*/%s-.*\.typelib' % re.escape(req.library)], +async def resolve_introspection_typelib_req(apt_mgr, req): + return await find_reqs_simple( + apt_mgr, + [r'/usr/lib/.*/girepository\\-.*/%s\\-.*\.typelib' + % re.escape(req.library)], regex=True) -def resolve_apt_req(apt_mgr, req): +async def resolve_apt_req(apt_mgr, req): # TODO(jelmer): This should be checking whether versions match as well. for package_name in req.package_names(): if not apt_mgr.package_exists(package_name): @@ -697,13 +845,47 @@ def resolve_apt_req(apt_mgr, req): return [req] -def resolve_boost_component_req(apt_mgr, req): - return find_reqs_simple( +async def resolve_boost_component_req(apt_mgr, req): + return await find_reqs_simple( apt_mgr, ["/usr/lib/.*/libboost_%s" % re.escape(req.name)], regex=True) -APT_REQUIREMENT_RESOLVERS = [ +async def resolve_kf5_component_req(apt_mgr, req): + return await find_reqs_simple( + apt_mgr, ["/usr/lib/.*/cmake/KF5%s/KF5%sConfig\\.cmake" % ( + re.escape(req.name), re.escape(req.name))], + regex=True) + + +async def resolve_vcs_access_req(apt_mgr, req): + PKG_MAP = { + 'hg': 'mercurial', + 'svn': 'subversion', + 'git': 'git', + 'bzr': 'bzr', + } + ret = [] + for vcs in req.vcs: + try: + ret.append(PKG_MAP[vcs]) + except KeyError: + logging.debug('Unable to map VCS %s to package', vcs) + return [AptRequirement.from_str(','.join(ret))] + + +async def resolve_oneof_req(apt_mgr, req): + options = await asyncio.gather( + *[resolve_requirement_apt(apt_mgr, req) for req in req.elements]) + for option in options: + if not option: + continue + return option + + +APT_REQUIREMENT_RESOLVERS: List[Tuple[ + Type[Requirement], Callable[ + [AptManager, Requirement], Awaitable[List[AptRequirement]]]]] = [ (AptRequirement, resolve_apt_req), (BinaryRequirement, resolve_binary_req), (VagueDependencyRequirement, resolve_vague_dep_req), @@ -736,6 +918,7 @@ APT_REQUIREMENT_RESOLVERS = [ (JDKRequirement, resolve_jdk_req), (JRERequirement, resolve_jre_req), (QTRequirement, resolve_qt_req), + (QtModuleRequirement, resolve_qt_module_req), (X11Requirement, resolve_x11_req), (LibtoolRequirement, resolve_libtool_req), (PerlModuleRequirement, resolve_perl_module_req), @@ -747,13 +930,19 @@ APT_REQUIREMENT_RESOLVERS = [ (CargoCrateRequirement, resolve_cargo_crate_req), (IntrospectionTypelibRequirement, resolve_introspection_typelib_req), (BoostComponentRequirement, resolve_boost_component_req), + (KF5ComponentRequirement, resolve_kf5_component_req), + (PHPExtensionRequirement, resolve_php_extension_req), + (OctavePackageRequirement, resolve_octave_pkg_req), + (VcsControlDirectoryAccessRequirement, resolve_vcs_access_req), + (OneOfRequirement, resolve_oneof_req), ] -def resolve_requirement_apt(apt_mgr, req: Requirement) -> List[AptRequirement]: +async def resolve_requirement_apt( + apt_mgr, req: Requirement) -> List[AptRequirement]: for rr_class, rr_fn in APT_REQUIREMENT_RESOLVERS: if isinstance(req, rr_class): - ret = rr_fn(apt_mgr, req) + ret = await rr_fn(apt_mgr, req) if not ret: return [] if not isinstance(ret, list): @@ -782,7 +971,8 @@ class AptResolver(Resolver): return "apt" def __repr__(self): - return "%s(%r, %r)" % (type(self).__name__, self.apt, self.tie_breakers) + return "%s(%r, %r)" % ( + type(self).__name__, self.apt, self.tie_breakers) @classmethod def from_session(cls, session, tie_breakers=None): @@ -808,8 +998,8 @@ class AptResolver(Resolver): apt_requirements.append(apt_req) if apt_requirements: self.apt.satisfy( - [PkgRelation.str(chain(*[r.relations for r in apt_requirements]))] - ) + [PkgRelation.str( + chain(*[r.relations for r in apt_requirements]))]) if still_missing: raise UnsatisfiedRequirements(still_missing) @@ -831,18 +1021,23 @@ class AptResolver(Resolver): [o for o, r in apt_requirements], ) + def resolve_all(self, req: Requirement): + return asyncio.run(resolve_requirement_apt(self.apt, req)) + def resolve(self, req: Requirement): - ret = resolve_requirement_apt(self.apt, req) + ret = self.resolve_all(req) if not ret: return None if len(ret) == 1: return ret[0] - logging.info("Need to break tie between %r with %r", ret, self.tie_breakers) + logging.info( + "Need to break tie between %r with %r", ret, self.tie_breakers) for tie_breaker in self.tie_breakers: winner = tie_breaker(ret) if winner is not None: if not isinstance(winner, AptRequirement): raise TypeError(winner) return winner - logging.info("Unable to break tie over %r, picking first: %r", ret, ret[0]) + logging.info( + "Unable to break tie over %r, picking first: %r", ret, ret[0]) return ret[0] diff --git a/ognibuild/resolver/dep_server.py b/ognibuild/resolver/dep_server.py new file mode 100644 index 0000000..cd80385 --- /dev/null +++ b/ognibuild/resolver/dep_server.py @@ -0,0 +1,88 @@ +#!/usr/bin/python3 +# Copyright (C) 2022 Jelmer Vernooij +# +# 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) diff --git a/ognibuild/session/__init__.py b/ognibuild/session/__init__.py index 464381d..0ef6a1a 100644 --- a/ognibuild/session/__init__.py +++ b/ognibuild/session/__init__.py @@ -69,12 +69,14 @@ 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) @@ -100,17 +102,26 @@ 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): + +def run_with_tee(session: Session, + args: List[str], **kwargs) -> Tuple[int, List[str]]: 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() @@ -121,7 +132,8 @@ def run_with_tee(session: Session, args: List[str], **kwargs): def get_user(session): - return session.check_output(["echo", "$USER"], cwd="/").decode().strip() + return session.check_output( + ["sh", "-c", "echo $USER"], cwd="/").decode().strip() def which(session, name): diff --git a/ognibuild/session/plain.py b/ognibuild/session/plain.py index df4b1a2..ab08e6c 100644 --- a/ognibuild/session/plain.py +++ b/ognibuild/session/plain.py @@ -20,6 +20,7 @@ from . import Session, NoSessionOpen, SessionAlreadyOpen import contextlib import os +import shutil import subprocess import tempfile from typing import Optional, Dict, List @@ -72,7 +73,8 @@ 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, @@ -84,13 +86,19 @@ 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) diff --git a/ognibuild/session/schroot.py b/ognibuild/session/schroot.py index bc9bcd1..7a485f2 100644 --- a/ognibuild/session/schroot.py +++ b/ognibuild/session/schroot.py @@ -66,25 +66,38 @@ 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"]) + subprocess.check_output( + ["schroot", "-c", self.chroot, "-b"], stderr=stderr) .strip() .decode() ) except subprocess.CalledProcessError: - # TODO(jelmer): Capture stderr and forward in SessionSetupFailure - raise SessionSetupFailure() + 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) logging.info( "Opened schroot session %s (from %s)", self.session_id, self.chroot ) @@ -156,24 +169,28 @@ 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="/") @@ -189,7 +206,8 @@ 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) @@ -203,13 +221,17 @@ 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) @@ -228,7 +250,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, dirs_exist_ok=True) + shutil.copytree(path, export_directory, symlinks=True) return export_directory, os.path.join(reldir, subdir) is_temporary = True diff --git a/ognibuild/test.py b/ognibuild/test.py index 750143f..2ab8261 100644 --- a/ognibuild/test.py +++ b/ognibuild/test.py @@ -15,16 +15,25 @@ # 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): +def run_test(session, buildsystems, resolver, fixers, log_manager=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() + for buildsystem in buildsystems: - buildsystem.test(session, resolver, fixers) + iterate_with_build_fixers( + fixers, log_manager.wrap( + partial(buildsystem.test, session, resolver))) return raise NoBuildToolsFound() diff --git a/ognibuild/upstream.py b/ognibuild/upstream.py new file mode 100644 index 0000000..8a08c90 --- /dev/null +++ b/ognibuild/upstream.py @@ -0,0 +1,253 @@ +#!/usr/bin/python3 +# Copyright (C) 2020-2021 Jelmer Vernooij +# +# 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 diff --git a/ognibuild/vcs.py b/ognibuild/vcs.py index 45b32c6..9115e28 100644 --- a/ognibuild/vcs.py +++ b/ognibuild/vcs.py @@ -43,7 +43,8 @@ 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: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fed528d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/releaser.conf b/releaser.conf deleted file mode 100644 index c4c19a4..0000000 --- a/releaser.conf +++ /dev/null @@ -1,14 +0,0 @@ -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" -} diff --git a/scripts/report-apt-deps-status b/scripts/report-apt-deps-status new file mode 100755 index 0000000..b0681d3 --- /dev/null +++ b/scripts/report-apt-deps-status @@ -0,0 +1,89 @@ +#!/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) diff --git a/setup.cfg b/setup.cfg index 07ea45f..e4e8584 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,13 +1,65 @@ +[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 - diff --git a/setup.py b/setup.py index 8167aae..eb46b40 100755 --- a/setup.py +++ b/setup.py @@ -1,40 +1,3 @@ -#!/usr/bin/env python3 -# encoding: utf-8 - +#!/usr/bin/python3 from setuptools import 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', - ) +setup() diff --git a/ognibuild/tests/__init__.py b/tests/__init__.py similarity index 85% rename from ognibuild/tests/__init__.py rename to tests/__init__.py index 075535d..71727f9 100644 --- a/ognibuild/tests/__init__.py +++ b/tests/__init__.py @@ -23,10 +23,13 @@ import unittest def test_suite(): names = [ - "debian_build", + 'buildlog', + 'logs', ] if os.path.exists("/usr/bin/dpkg-architecture"): + names.append("debian_build") names.append("debian_fix_build") - module_names = ["ognibuild.tests.test_" + name for name in names] + names.append("resolver_apt") + module_names = ["tests.test_" + name for name in names] loader = unittest.TestLoader() return loader.loadTestsFromNames(module_names) diff --git a/tests/test_buildlog.py b/tests/test_buildlog.py new file mode 100644 index 0000000..e1d19d9 --- /dev/null +++ b/tests/test_buildlog.py @@ -0,0 +1,47 @@ +#!/usr/bin/python +# Copyright (C) 2022 Jelmer Vernooij +# +# 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") diff --git a/ognibuild/tests/test_debian_build.py b/tests/test_debian_build.py similarity index 72% rename from ognibuild/tests/test_debian_build.py rename to tests/test_debian_build.py index 0b06869..fe656cd 100644 --- a/ognibuild/tests/test_debian_build.py +++ b/tests/test_debian_build.py @@ -17,8 +17,17 @@ import datetime import os +import sys -from ..debian.build import add_dummy_changelog_entry, get_build_architecture +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 breezy.tests import TestCaseWithTransport, TestCase @@ -150,3 +159,43 @@ 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")) diff --git a/ognibuild/tests/test_debian_fix_build.py b/tests/test_debian_fix_build.py similarity index 72% rename from ognibuild/tests/test_debian_fix_build.py rename to tests/test_debian_fix_build.py index c0a5456..ba28ffc 100644 --- a/ognibuild/tests/test_debian_fix_build.py +++ b/tests/test_debian_fix_build.py @@ -29,13 +29,15 @@ from buildlog_consultant.common import ( MissingRubyGem, MissingValaPackage, ) -from ..debian.apt import AptManager, FileSearcher -from ..debian.fix_build import ( +from ognibuild.debian.apt import AptManager, FileSearcher +from ognibuild.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 @@ -44,7 +46,7 @@ class DummyAptSearcher(FileSearcher): def __init__(self, files): self._apt_files = files - def search_files(self, path, regex=False, case_insensitive=False): + async 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 @@ -97,7 +99,7 @@ blah (0.1) UNRELEASED; urgency=medium self._apt_files = {} def resolve(self, error, context=("build",)): - from ..session.plain import PlainSession + from ognibuild.session.plain import PlainSession session = PlainSession() apt = AptManager(session) @@ -109,7 +111,8 @@ 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): @@ -118,7 +121,8 @@ 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 = { @@ -130,7 +134,8 @@ 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()) @@ -153,10 +158,12 @@ 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()) @@ -173,7 +180,8 @@ 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( @@ -200,28 +208,34 @@ 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() ) @@ -232,3 +246,63 @@ 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 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 ", + 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') diff --git a/tests/test_logs.py b/tests/test_logs.py new file mode 100644 index 0000000..6fc4e65 --- /dev/null +++ b/tests/test_logs.py @@ -0,0 +1,95 @@ +#!/usr/bin/python +# Copyright (C) 2022 Jelmer Vernooij +# +# 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()) diff --git a/tests/test_resolver_apt.py b/tests/test_resolver_apt.py new file mode 100644 index 0000000..abcad3f --- /dev/null +++ b/tests/test_resolver_apt.py @@ -0,0 +1,47 @@ +#!/usr/bin/python +# Copyright (C) 2022 Jelmer Vernooij +# +# 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')) From c3962131bbf56e5d77d1637506b39cedced3c944 Mon Sep 17 00:00:00 2001 From: Tianyu Chen Date: Tue, 22 Nov 2022 11:26:25 +0800 Subject: [PATCH 36/40] update upstream signing-key --- debian/upstream/signing-key.asc | 1510 +++++++++++++++++-------------- 1 file changed, 844 insertions(+), 666 deletions(-) diff --git a/debian/upstream/signing-key.asc b/debian/upstream/signing-key.asc index bee2dd6..32b347e 100644 --- a/debian/upstream/signing-key.asc +++ b/debian/upstream/signing-key.asc @@ -11,670 +11,848 @@ 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 -fHg2YJU2Pedn7TogRIhcWeU8ZiFYFNrsjCmj2hfKoK5Z7b50ywQokdnhXVWtb05D -+BVXrO7CPIt1mjMQpV2xgAear8DUThcsN/n6T6SrsIbgBfYkAAKdO4Qra4bmZmdv -Q323rYvu0MPwsciYReJ6i3qosoI4rUKkxZb3QbZ0OY0DkthVy/U6PmL+nJvsFQBP -jLqKhuGR4Itswbfh1Zn+VQ/qPUnPjR+yNlCMKnRNdT5GWn7XA6hlq3rQnYU3VYfo -tCVKZWxtZXIgVmVybm9vaWogPGplbG1lckBubC5saW51eC5vcmc+iQI8BDABAgAm -BQJNb7RSHx0gbmwubGludXgub3JnIG5vIGxvbmdlciBleGlzdHMACgkQAIBvK9cp -pFcOOw//VRlFHgGefSIkxg381wo8t38g7NXbLwGtjxvJjnrwnxQSVnesGxpZHgyo -/Tq0Ly+PPYtPzPUX2rbVw90Jx6gD2Yl7I6rnYoNurNhJqGsGRIyCQ11UdKmDu5jg -PEurEjycxZK1UOkqnfrVeCubDilFNgTuvOQaC1rw51M00sqv/8KvkhWE4ZGkvgOe -GKLe5NbxNuBTiPOyRIVWJD2q42LCaayYrcM/1cIZk55dMePiSCHkDLg0GYMdkbhs -ohE8YBr3PWm5r6LGI15ihm43YawM4kVdgSbvDB5kn11jNuOeMLqVT8VCfhN8V9J/ -oPfd8n4EZchvwiupGCi8iR5uwWIg4WOQL8dT/uuCPM0fZeZYU5oxcvjmT48FSTzZ -HOfXg8HhzhlNap4tQS+cvHleUmDvImXo/Dn9L20v5OgwBu/Wq8lREBVZjuEUxp4a -+G9v6SMhtYkFetWnAvCui3DOQKlmiL6GKpUOx3ZbXBe9uSsjMsahhhHjyc4MRsFj -4BcxCc3IMK/p8RWvrrl3l9bJlQrRsxWbT95fVPnUIqG7O+8Ttxl+oIuubnKBn3nZ -8lWBbgWbM7PeVHJNyfV91KQp/xaHV0sWLlwLmN0S5GAuqhN7gyPMHdCgfLuUN42i -AkUqehVSfJyFX0qCaoqUiIeLDrtC1f5jmbR4maiIuuUmv/p6vqu0JkplbG1lciBW -ZXJub29paiA8amVsbWVyQGNhbm9uaWNhbC5jb20+iQIfBDABAgAJBQJQlSLBAh0g -AAoJEACAbyvXKaRXQlcP/A1iUm2F6If39VmiYUa3ZA9ESi3ABtXc9OatlKzFlw/4 -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 -=uqnI +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 +rLCfVWub9fkSQcR9UiPYGRpi2VKZBnZARUqj4b76tCJKZWxtZXIgVmVybm9vaWog +PGplbG1lckBzZXJuZXQuZGU+iQI7BDABCgAlBQJU3m8OHh0gTm8gbG9uZ2VyIHdv +cmtpbmcgZm9yIFNlck5ldAAKCRAAgG8r1ymkVwiCD/9Lt1XSCMadKZrZPfo0y9Np +lkEgOxfxycOvnKYrZh+xBynJRDhqGyEQ7XDli5KIO209IIfuRVXvP/pZgb1rpZRa +oiqVGtCYD6ITXGzNLwwyPFsdSh05uco7dYS6Cbkvkkwtql858816a+TupnPlO3Ve +DzEEm/368Rx0NdNaMoDirNqSPClOOeIiJN+1vl3GyY9uMCP7P3dAG6q7GsowLbm8 +AjYnBDJb3XJK8u85l09NqtB3qyuOIINtBC/QFK6+Svb2UcFKdOpztNqQCd4Rf6k/ +o1ASlHBBr3o2RkRQr2LIk8/HFiOOJzkMGwfTDXOO3BueHpqWCmFU0TQw2gwxhLPA +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 +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 +bkDK4ZnmjQm+UWfMojpoEwyCxTqzVLNlQfy8M+/vMag7DYjo14EBOvhgEIXGVk+E +f3T7w8GW+Mj4Re4UFiJyANSTG0oNdUPna2oMlNFHdB0eiaEC5XVJ9OlyxukWL5lV +QTeosf9ot5M6tD/pPqtcyj8zML2JfISKtCVKZWxtZXIgVmVybm9vaWogPGplbG1l +ckBqZWxtZXIuY28udWs+iQJUBBMBCgA+AhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4B +AheAFiEE3IN+4Up+NzR+hwYXAIBvK9cppFcFAmDrkoEFCRlgswAACgkQAIBvK9cp +pFcshA//dO+lmT0hzvjaFWw5YcQUx9ce3ngLdm2ooMHP498Zaiee0W1aRN4F82G+ +akgIy0AdNKmGW9/LVImngCtunQAQkv+OoS4fapOW0sgRzOltBNBZckXewOjg7DCG +L9/5voUpU8fkk4Fx2H/kGliq6I9VNBIZrrLOkiOLPvsnMw+FD7/XaDVWt+6QuWEK +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 +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 +uwFmF5mevYO27rIdZyWaU3k+iblrTKzwsROFhTOvAWJhN7WhwOrfAGUpxUKwc9Em +itX+7mib0MaK0UnIZtHR6SSbr9wpATaHhCaQ5ifW6ps3zsqW3fa4A6nIZ+leaG8e +GBaIVU8RcqBlRFFu4pw8wYqRnIEFvsOh0YopLBMP9OtZmWjKbzV4CyYwMNbVdjaU +CMf9lZvKlBVRDM2e9k5RRuq/Nbf/dhKUuqYh8vASpcL8b911+f5n1LFneMLXrWSC +zS2msT2F1I/JyVV1oQdPZXzsa14sHlwBvHcGCQiZChBuJJGD1mPT4Wuz0SzQSzGJ +f2EB9teqhFw0XVC0K40y9VVhkW1lRe+W441w0qFm725FNi1Cycm5JSeAEluOrT0V +tyJYuIWoNEpvbdCpNP5LT7lJ7uHv96C69aGGSYaE4PQ+CcO/l9tUAF3chOPxViM1 +L0MgsBx9IXgtmqMd8xEw/NHHErn0pg8WhWaXiQJ1BCgBCgBfFiEE3IN+4Up+NzR+ +hwYXAIBvK9cppFcFAlns7CtBHQJodHRwczovL3d3dy55dWJpY28uY29tL3N1cHBv +cnQvc2VjdXJpdHktYWR2aXNvcmllcy95c2EtMjAxNy0wMS8ACgkQAIBvK9cppFdh +wA//WcTBgLqimYVJJIvEcdl/GdQtOLl5bZY2oX1SEXpx7Fj47MiMn1DHS1e0G1e7 +ZOQ9NWNTfIFvQHDCtUCfS+VbdQshkzEwZdwJzC9FAZLtGs7W0KqvmoNjgSTokLl4 +21wna6WbgYkgKC4OYCD4YpsojueT6YpDouO9/d0QKtTrY/oGYVXAKockLMwwGZcm +atavjsppBZRGKtZ2kbPZHaw42jgb0OBDKRcolWp1+R0SE7CF9JBOGYXmFvcTtx0s +xf2L8yDjthI7i0A4CGdP/jfTTqsNrT1pn3psNmB6OMdL0qipcJ3/r5bRB/50je/J +Yo3Sgo8BfH+F6bn6w+dpiN+uqDIXZftXCYwIyguG68RAxNTCdJkafL5qbR7TOoLH +V6FyawcnlxdASIsm7SsTttyXtv7QxTd8pOfbrZQikUpLXbIUVGnElthzGsDmtxiC +fiR8fkFBLgQefiQXTlB+Y4s5TIfCQX5JOCwErd3TPk+B8tRkxAGrrfi3y0XS2y2z +BhFvysAwAZ+5XpKloBPxT7P9A83oIFF2fzl4vcMgVFK2JaIhBG9Foy+iFpfWObRl +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 +vA== +=e6vU -----END PGP PUBLIC KEY BLOCK----- From 95de606b69f541fd18d92cd781e4742b4970e02d Mon Sep 17 00:00:00 2001 From: Tianyu Chen Date: Tue, 22 Nov 2022 11:38:14 +0800 Subject: [PATCH 37/40] changelog 0.0.15 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 34e62bd..e754ef7 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +ognibuild (0.0.15-0.1) UNRELEASED; urgency=low + + * New upstream release. + + -- Tianyu Chen Tue, 22 Nov 2022 11:12:48 +0800 + ognibuild (0.0.7-1) UNRELEASED; urgency=low * New upstream release. From 91de60be90573d6e1c3dafb785f8ee5d76fb60a4 Mon Sep 17 00:00:00 2001 From: Tianyu Chen Date: Tue, 22 Nov 2022 11:45:54 +0800 Subject: [PATCH 38/40] Bump debhelper from old 12 to 13. Changes-By: lintian-brush Fixes: lintian: package-uses-old-debhelper-compat-version See-also: https://lintian.debian.org/tags/package-uses-old-debhelper-compat-version.html --- debian/changelog | 1 + debian/control | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index e754ef7..e78f6db 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,6 +1,7 @@ ognibuild (0.0.15-0.1) UNRELEASED; urgency=low * New upstream release. + * Bump debhelper from old 12 to 13. -- Tianyu Chen Tue, 22 Nov 2022 11:12:48 +0800 diff --git a/debian/control b/debian/control index cb11c07..6a2d75d 100644 --- a/debian/control +++ b/debian/control @@ -1,6 +1,6 @@ Rules-Requires-Root: no Standards-Version: 4.5.1 -Build-Depends: debhelper-compat (= 12), +Build-Depends: debhelper-compat (= 13), dh-sequence-python3, python3-all, python3-apt, From a83e2bf112a983fa69afc4a0a8941c9f7740929c Mon Sep 17 00:00:00 2001 From: Tianyu Chen Date: Tue, 22 Nov 2022 11:46:04 +0800 Subject: [PATCH 39/40] Update standards version to 4.6.1, no changes needed. Changes-By: lintian-brush Fixes: lintian: out-of-date-standards-version See-also: https://lintian.debian.org/tags/out-of-date-standards-version.html --- debian/changelog | 1 + debian/control | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index e78f6db..9a48a55 100644 --- a/debian/changelog +++ b/debian/changelog @@ -2,6 +2,7 @@ ognibuild (0.0.15-0.1) UNRELEASED; urgency=low * New upstream release. * Bump debhelper from old 12 to 13. + * Update standards version to 4.6.1, no changes needed. -- Tianyu Chen Tue, 22 Nov 2022 11:12:48 +0800 diff --git a/debian/control b/debian/control index 6a2d75d..22fa4ba 100644 --- a/debian/control +++ b/debian/control @@ -1,5 +1,5 @@ Rules-Requires-Root: no -Standards-Version: 4.5.1 +Standards-Version: 4.6.1 Build-Depends: debhelper-compat (= 13), dh-sequence-python3, python3-all, From acae5aef843ec94b57a1506b0f3cfe80881c807b Mon Sep 17 00:00:00 2001 From: Tianyu Chen Date: Tue, 22 Nov 2022 12:04:26 +0800 Subject: [PATCH 40/40] Non-maintainer upload --- debian/changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/changelog b/debian/changelog index 9a48a55..45290ce 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,5 +1,6 @@ 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.