Compare commits

..

185 Commits
0.190 ... main

Author SHA1 Message Date
Simon Quigley
466e2784de Upload to Unstable 2025-03-04 13:43:32 -06:00
Simon Quigley
ba3f0511f9 syncpackage: Catch exceptions cleanly, simply skipping to the next package (erring on the side of caution) if there is an error doing the download (LP: #1943286). 2025-03-04 13:42:50 -06:00
Simon Quigley
2e550ceff2 syncpackage: Cache the sync blocklist in-memory, so it's not fetched multiple times when syncing more than one package. 2025-03-04 13:39:07 -06:00
Simon Quigley
6c8a5d74bd syncpackage: s/syncblacklist/syncblocklist/g 2025-03-04 13:29:02 -06:00
Simon Quigley
3d11516599 mk-sbuild: default to using UTC for schroots (LP: #2097159). 2025-03-04 13:22:40 -06:00
Simon Quigley
5a20308ab1 Read ~/.devscripts in a more robust way, to ideally pick up multi-line variables (Closes: #725418). 2025-03-04 13:17:30 -06:00
Simon Quigley
b551877651 Add a changelog entry 2025-03-04 13:10:04 -06:00
ferbraher
4a4c4e0a27 Parsing arch parameter to getBinaryPackage() 2025-03-04 13:08:59 -06:00
Simon Quigley
865c1c97bc Add a changelog entry 2025-03-04 13:07:42 -06:00
Shengjing Zhu
d09718e976 import-bug-from-debian: package option is overridden and not used 2025-03-04 13:07:11 -06:00
Simon Quigley
bff7baecc9 Add a changelog entry 2025-03-04 13:06:38 -06:00
Dan Bungert
45fbbb5bd1 mk-sbuild: enable pkgmaintainermangler
mk-sbuild installs pkgbinarymangler into the schroot.  Of of the
provided tools in pkgbinarymangler is pkgmaintainermangler.
pkgmaintainermangler is disabled by default, and enabled with
configuration.

A difference between launchpad builds of a synced package and an sbuild
is that the maintainer information will be different.

Enable pkgmaintainermangler to close this difference.
2025-03-04 13:05:57 -06:00
Simon Quigley
ca217c035e Add a new changelog entry 2025-03-04 13:04:49 -06:00
Simon Quigley
b5e117788b Upload to Unstable 2025-03-01 11:30:18 -06:00
Simon Quigley
ddba2d1e98 Update Standards-Version to 4.7.2, no changes needed. 2025-03-01 11:29:53 -06:00
Simon Quigley
02d65a5804 [syncpackage] Do not use exit(1) on an error or exception unless it applies to all packages, instead return None so we can continue to the next package. 2025-03-01 11:26:59 -06:00
Simon Quigley
bda85fa6a8 [syncpackage] Add support for -y or --yes, noted that it should be used with care. 2025-03-01 11:22:52 -06:00
Simon Quigley
86a83bf74d [syncpackage] Within fetch_source_pkg, do not exit(1) on an error or exception, simply return None so we can continue to the next package. 2025-03-01 11:17:02 -06:00
Simon Quigley
162e758671 [syncpackage] When syncing multiple packages, if one of the packages is in the sync blocklist, do not exit, simply continue. 2025-03-01 11:12:49 -06:00
Simon Quigley
049425adb7 Add debian/files to .gitignore 2025-03-01 11:11:34 -06:00
Simon Quigley
f6ca6cad92 Add a new changelog entry 2025-03-01 11:11:17 -06:00
Simon Quigley
3dc17934d6 Upload to Unstable 2025-02-24 19:55:03 -06:00
Simon Quigley
10a176567a Remove mail line from default ~/.sbuildrc, to resolve the undeclared dependency on sendmail (Closes: #1074632). 2025-02-24 19:52:59 -06:00
Simon Quigley
86b366c6c5 Add a large warning at the top of mk-sbuild encouraging the use of the unshare backend. This is to provide ample warning to users. 2025-02-24 19:15:55 -06:00
Simon Quigley
50b580b30e Add a manpage for running-autopkgtests. 2025-02-24 18:51:12 -06:00
Simon Quigley
6ba0641f63 Rename bitesize to lp-bitesize (Closes: #1076224). 2025-02-24 18:51:10 -06:00
Simon Quigley
1e815db9d2 Add my name to the copyright file. 2025-02-24 18:35:20 -06:00
Simon Quigley
e2f43318bd Add several Lintian overrides related to .pyc files. 2025-02-24 18:34:18 -06:00
Julien Plissonneau Duquène
cdd81232d9 Fix reverse-depends -b crash on packages that b-d on themselves (Closes: #1087760). 2025-02-24 18:31:33 -06:00
Simon Quigley
65044d84d9 Update Standards-Version to 4.7.1, no changes needed. 2025-02-24 18:26:59 -06:00
Mattia Rizzolo
19e40b49c2
Fix minor typo in pbuilder-dist(1)
LP: #2096956
Thanks: Rolf Leggewie for the patch
Signed-off-by: Mattia Rizzolo <mattia@debian.org>
2025-01-30 07:52:22 +01:00
Benjamin Drung
55eb521461 Release 0.203 2024-11-02 18:20:32 +01:00
Benjamin Drung
983bb3b70e Depend on python3-yaml for pm-helper 2024-11-02 18:09:16 +01:00
Benjamin Drung
85f2e46f7d conform to snake_case naming style 2024-11-02 18:07:23 +01:00
Benjamin Drung
649c3db767 ubuntu-build: fix used-before-assignment
```
ubuntu-build:244:40: E0601: Using variable 'necessary_privs' before assignment (used-before-assignment)
```
2024-11-02 17:56:47 +01:00
Benjamin Drung
e7ba650414 Avoid unnecessary "elif" after "continue"
Address pylint's no-else-continue.
2024-11-02 17:55:33 +01:00
Benjamin Drung
3bc802a209 Use lazy % formatting in logging functions 2024-11-02 17:55:20 +01:00
Benjamin Drung
92c80d7bb7 ubuntu-build: remove unused code/imports 2024-11-02 17:54:06 +01:00
Benjamin Drung
d7362d9ed8 Use Python f-strings
```
flynt -ll 99 -tc -tj -a pbuilder-dist pm-helper running-autopkgtests ubuntu-build ubuntutools
```
2024-11-02 17:49:20 +01:00
Benjamin Drung
c7a855ff20 Format code with black and isort
```
isort pbuilder-dist pm-helper running-autopkgtests ubuntu-build ubuntutools
black -C pbuilder-dist pm-helper running-autopkgtests ubuntu-build ubuntutools
```
2024-11-02 17:21:30 +01:00
Benjamin Drung
017941ad70 setup.py: add pm-helper 2024-11-02 16:41:44 +01:00
Benjamin Drung
69914f861e add missing files to debian/copyright 2024-11-02 16:35:31 +01:00
Benjamin Drung
454f1e30c8 Bump year in copyright 2024-11-02 15:57:19 +01:00
Benjamin Drung
55bc403a95 Bump Standards-Version to 4.7.0 2024-11-02 15:56:01 +01:00
Benjamin Drung
c9339aeae4 import-bug-from-debian: add type hints 2024-11-02 15:34:59 +01:00
Benjamin Drung
c205ee0381 import-bug-from-debian: avoid type change of bug_num
The variable `bug_num` has the type `str`. Do not reuse the name for
type `int` to ease mypy.
2024-11-02 15:33:15 +01:00
Benjamin Drung
7577e10f13 import-bug-from-debian: reuse message variable
`log[0]["message"]` was already queried.
2024-11-02 15:32:19 +01:00
Florent 'Skia' Jacquet
e328dc05c2 import-bug-from-debian: split big main function into smaller ones
This allows better understanding of the various parts of the code, by
naming important parts and defining boundaries on the used variables.
2024-11-02 15:08:09 +01:00
Florent 'Skia' Jacquet
9a94c9dea1 import-bug-from-debian: handle multipart messages
With multipart messages, like #1073996, `import-bug-from-debian` would
produce bug description with this:
```
[<email.message.Message object at 0x7fbe14096fa0>, <email.message.Message object at 0x7fbe15143820>]
```
For that kind of bug, it now produces a correct description with the
plain text parts concatenated in the description, the attachments added
as attachments, and the inline images converted to attachments with an
inline message placeholder.

See #981577 for a particularly weird case now gracefully handled.
If something weirder happens, then the tool will now abort with a clear
message instead of producing garbage.

Closes: #969510
2024-11-02 14:57:01 +01:00
Florent 'Skia' Jacquet
47ab7b608b Add gitignore 2024-10-30 17:31:54 +01:00
Steve Langasek
56044d8eac Recommend sbuild over pbuilder. sbuild is the tool recommended by Ubuntu developers whose behavior most closely approximates Launchpad builds. 2024-05-26 13:04:55 -07:00
Steve Langasek
c523b4cfc4 open new version 2024-05-26 13:01:23 -07:00
Steve Langasek
3df40f6392 Handle exceptions on retry
The "can be retried" value from launchpad may have been cached.  Avoid an
exception when we race someone else retrying a build.
2024-05-26 12:57:14 -07:00
Simon Quigley
6ebffe3f4a Consolidate Ubuntu changelog entries, upload to Unstable 2024-04-12 23:35:08 -05:00
Chris Peterson
f01234e8a5 update debian/copyright
- Correctly add ISC licenses to new files in ubuntutools/tests/*
  as specified in debian/copyright
- Add GPL-3 licenses and correct attribution for:
    - running-autopkgtests
    - ubuntutools/running_autopkgtests.py
2024-03-13 09:21:30 -07:00
Chris Peterson
43891eda88 depends: python3-launchpadlib-desktop
Replace the dependency on python3-launchpadlib with
python3-launchpadlib-desktop. This package is the same as python3-launchpadlib
except that it also includes python3-keyring, which is a requirement for
some of the desktop-centric code-paths. In the case, requestsync has a
path for logging in via a web browser which also requires python3-keyring
to be installed. This had caused a ModuleNotFoundError when
python3-launchpadlib dropped python3-keyring from Recommends to Suggests
(LP: #2049217).
2024-03-13 09:17:49 -07:00
Steve Langasek
132866e2ba releasing package ubuntu-dev-tools version 0.201ubuntu2 2024-03-12 17:03:58 -07:00
Steve Langasek
a0fcac7777 changelog update 2024-03-12 17:03:41 -07:00
Steve Langasek
490895075d Merge latest Ubuntu upload 2024-03-12 17:01:59 -07:00
Chris Peterson
5186e76d8d Import Debian version 0.201ubuntu1
ubuntu-dev-tools (0.201ubuntu1) noble; urgency=medium
.
  * Replace Depends on python3-launchpadlib with Depends on
    python3-launchpadlib-desktop (LP: #2049217)
2024-03-12 17:01:19 -07:00
Steve Langasek
bf46f7fbc1 Fix license statement in manpage 2024-03-12 12:09:19 -07:00
Steve Langasek
881602c4b9 Update ubuntu-build manpage to match current options 2024-03-12 12:08:58 -07:00
Steve Langasek
c869d07f75 ubuntu-build: don't retry builds Launchpad tells us can't be retried 2024-03-12 11:52:32 -07:00
Gianfranco Costamagna
59041af613 update changelog 2024-03-12 10:39:36 +01:00
Gianfranco Costamagna
0ec53180f2 Merge remote-tracking branch 'vorlon/ubuntu-build-revamp' 2024-03-12 10:36:13 +01:00
Steve Langasek
c92fa6502f ubuntu-build: Handling of proposed vs release pocket default for ppas 2024-03-10 21:43:06 -07:00
Steve Langasek
07d3158ade Don't do expensive check of group membership on rescore, just handle exceptions
This could do with some further refactoring, but will probably postpone that
until a decision is made about dropping the non-batch mode
2024-03-10 16:51:15 -07:00
Steve Langasek
d5faa9b133 Proper handling of getDevelopmentSeries() 2024-03-10 15:48:16 -07:00
Steve Langasek
9e710a3d66 Always use exact match when looking for source packages by name 2024-03-10 15:46:22 -07:00
Steve Langasek
010af53d7c Add a -A archive option to act on ppas as well.
This results in a major refactor of the code to use launchpadlib directly
instead of the ubuntutools.lp.lpapicache module in ubuntu-dev-tools which is
idiosyncratic and does not expose the full launchpad API.  Easier to rewrite
to use the standard library.
2024-03-10 14:35:47 -07:00
Steve Langasek
0bef4d7352 ubuntu-build: fix licensing.
Canonical licensing policy has never been GPLv3+, only GPLv3.
2024-03-10 13:36:30 -07:00
Steve Langasek
688202a7cf ubuntu-build: update copyright 2024-03-10 13:35:56 -07:00
Steve Langasek
691c1381db ubuntu-build: support retrying builds in other states that failed-to-build 2024-03-10 01:45:20 -08:00
Steve Langasek
f01502bda2 ubuntu-build: make the --arch option top-level
This gets rid of the fugly --arch2 option
2024-03-08 18:53:20 -08:00
Steve Langasek
42f8e5c0d2 ubuntu-build: in batch mode, print a count of packages retried 2024-03-08 18:38:59 -08:00
Steve Langasek
bb8a9f7394 ubuntu-build: support --batch with no package names to retry all 2024-03-08 16:43:30 -08:00
Gianfranco Costamagna
a058c716b9 Upload to sid 2024-02-29 22:49:37 +01:00
Chris Peterson
e64fe7e212 Update Changelog 2024-02-29 13:08:13 -08:00
Chris Peterson
f07d3df40c running-autopkgtests: make running-autopkgtests available
Previously running-autopkgtests was added to the source but
wasn't correctly added to the scripts in setup.py, so it wasn't
actually available in the installed package. This also adds the
script to the package description.
2024-02-29 13:06:12 -08:00
Gianfranco Costamagna
f73f2c1df1 Upload to sid 2024-02-15 18:09:28 +01:00
Gianfranco Costamagna
268d082226 Update changelog 2024-02-15 18:06:49 +01:00
Athos Ribeiro
6bc59d789e Log syncpackage LP auth errors before halting 2024-02-15 18:06:38 +01:00
Logan Rosen
9a4cc312f4 Don't rely on debootstrap for validating Ubuntu distro 2024-02-15 17:51:35 +01:00
Ying-Chun Liu (PaulLiu)
ffc787b454 Drop qemu-debootstrap
qemu-debootstrap is deprecated for a while. In newer qemu release
the command is totally removed. We can use debootstrap directly.

Signed-off-by: Ying-Chun Liu (PaulLiu) <paulliu@debian.org>
2024-02-15 17:49:59 +01:00
Chris Peterson
bce1ef88c5 running-autopkgtests: use f-strings 2024-02-14 15:19:48 -08:00
Chris Peterson
a9eb902b83 running-autopkgtests: Changelog entry, ArgumentParser, refactor, tests
Created a new changelog entry to include addition of the running-autopkgtests
script. This includes a refactor of the original script resulting in a new
module in ubuntutools, test cases, and the addition an argument parser to
allow printing just the queued tests, just the running tests, or both
(default).
2024-02-14 15:19:43 -08:00
Chris Peterson
cb7464cf61 Add running-autopkgtests script
This script will print out all of the running and queued autokpgtests.
Originally this was a script titled lp-test-isrunning
from lp:~ubuntu-server/+git/ubuntu-helpers.
2024-02-14 14:55:33 -08:00
Simon Quigley
19f1df1054 Upload to Unstable 2024-01-29 10:03:47 -06:00
Simon Quigley
7f64dde12c Add a changelog entry for Steve 2024-01-29 10:03:19 -06:00
Simon Quigley
c2539c6787 Add a changelog entry for adding myself to Uploaders. 2024-01-29 09:59:30 -06:00
Simon Quigley
fd885ec239 Merge remote-tracking branch 'vorlon/pm-helper' 2024-01-29 09:57:52 -06:00
Simon Quigley
abbc56e185 Add my name to Uploaders.
To be fair, the last four uploads should have started with "Team upload." Whoops.
2024-01-10 20:21:06 -06:00
Simon Quigley
a2176110f0 Upload to Unstable. 2024-01-10 20:04:15 -06:00
Simon Quigley
a5185e4612 Add proper support for virtual packages in check-mir, basing the determination solely off of binary packages. This is not expected to be a typical case. 2024-01-10 20:03:44 -06:00
Simon Quigley
e90ceaf26b In check-mir, ignore debhelper-compat when checking the build dependencies. This is expected to be a build dependency of all packages, so warning about it in any way is surely a red herring. 2024-01-10 19:56:06 -06:00
Simon Quigley
47fd5d7cca Upload to Unstable. 2023-10-03 14:01:44 -05:00
Simon Quigley
2f396fe549 When using pull-*-source to grab a package which already has a defined Vcs- field, display the exact same warning message apt source does. 2023-10-03 14:01:19 -05:00
Gianfranco Costamagna
5bda35f6b4 Update also syncpackage help 2023-08-25 20:04:12 +02:00
Simon Quigley
db916653cd Update the manpage for syncpackage to reflect the ability to sync multiple packages at once. 2023-08-10 14:39:01 -05:00
Simon Quigley
784e7814e9 Allow the user to sync multiple packages at one time (LP: #1756748). 2023-08-04 14:38:46 -05:00
Simon Quigley
bed2dc470d Add support for the non-free-firmware components in all tools already referencing non-free. 2023-07-26 13:04:12 -05:00
Gianfranco Costamagna
414bc76b50 Upload to Debian 2023-07-08 08:43:09 +02:00
Gianfranco Costamagna
6f0caf1fc0 ubuntu-build: For some reasons, now you need to be authenticated before trying to use the "PersonTeam" class features.
Do it at the begin instead of replicating the same code inside the tool itself.

This fixes e.g. this failure:

./ubuntu-build --batch --retry morsmall
Traceback (most recent call last):
  File "/tmp/ubuntu-dev-tools/ubuntu-build", line 317, in <module>
    main()
  File "/tmp/ubuntu-dev-tools/ubuntu-build", line 289, in main
    can_retry = args.retry and me.canUploadPackage(
AttributeError: 'NoneType' object has no attribute 'canUploadPackage'
2023-07-07 19:23:41 +02:00
Robie Basak
4bcc55372a Changelog for 0.193ubuntu5 2023-07-06 11:28:21 +01:00
Robie Basak
232a73de31 ubuntutools/misc: swap iter_content for raw stream
This is a partial revert of 1e20363.

When downloading a .diff.gz source package file, we do expect it to be
written to disk still compressed. If we were to uncompress it, then we
would get a size mismatch and even if we were to ignore that, we'd get a
hash mismatch.

On the other hand when downloading a changes file we need to make sure
that is written to disk uncompressed.

To make this work in both cases we can ask the HTTP server for no
special content encoding using "Accept-Encoding: identity". This is what
wget requests, for example. Then we can write the output to the file
without performing any decoding at our end by using the raw response
object again.

This fixes both cases.

LP: #2025748
2023-07-06 11:28:21 +01:00
Steve Langasek
9aab0135a2 Add an initial manpage for pm-helper 2023-06-14 17:01:36 -07:00
Steve Langasek
23539f28b1 Update license header 2023-06-14 16:52:56 -07:00
Steve Langasek
4a09d23db6 There is no dry-run mode 2023-06-14 16:29:43 -07:00
Steve Langasek
534cd254f4 typo update-excuses->update-excuse 2023-06-14 15:14:14 -07:00
Steve Langasek
29c3fa98bc Use a context manager for lzma 2023-06-14 15:13:46 -07:00
Steve Langasek
7c9c7f2890 Sensible behavior when called for a non-existent package name 2023-06-14 15:12:57 -07:00
Steve Langasek
739279da3f More pythonic function name (thanks, Bryce) 2023-06-14 14:51:15 -07:00
Steve Langasek
7c11832ee0 Sensible behavior when a requested package isn't in -proposed. 2023-06-14 14:01:53 -07:00
Steve Langasek
f5512846d6 Code refactor; thanks, Bryce 2023-06-14 13:59:25 -07:00
Steve Langasek
9e0dff4461 move from OptionParser to ArgumentParser 2023-06-14 13:57:14 -07:00
Steve Langasek
7129e6e27a Fix imports 2023-06-13 13:57:47 -07:00
Steve Langasek
79d30a9bfc Add dependency on dateutil 2023-06-13 13:52:18 -07:00
Steve Langasek
2c6a8b5451 Initial implementation of pm-helper
This is a tool for making it easier to identify the next thing to work on
for proposed-migration.
2023-06-13 13:48:28 -07:00
Steve Langasek
ad014685ea Import utils.py from ubuntu-archive-tools 2023-06-13 13:47:15 -07:00
Steve Langasek
ff1c95e2c0 Remove references to architectures not supported in any active Ubuntu release. 2023-05-30 21:05:56 -07:00
Steve Langasek
89e788bf48 Remove references to deprecated http://people.canonical.com/~ubuntu-archive. 2023-05-30 19:37:11 -07:00
Steve Langasek
a000e9db5e releasing package ubuntu-dev-tools version 0.193ubuntu4 2023-05-30 10:02:47 -07:00
Steve Langasek
83158d24d9 Merge staged changes 2023-05-30 10:00:57 -07:00
Steve Langasek
6e6e1f1e1a Excise all references to cdbs (including in test cases) 2023-05-30 10:00:17 -07:00
Steve Langasek
c7a7767339 Fix a typo introduced in the last upload that made mk-sbuild fail unconditionally. LP: #2017177. 2023-05-30 09:55:06 -07:00
Steve Langasek
ac2f980e0f Remove references to ftpmaster.internal. When this name is resolvable but firewalled, syncpackage hangs; and these are tools for developers, not for running in an automated context in the DCs where ftpmaster.internal is reachable. 2023-04-12 17:59:40 -07:00
Steve Langasek
ccab82e054 releasing package ubuntu-dev-tools version 0.193ubuntu1 2023-04-12 09:45:23 -07:00
Steve Langasek
2e4e8b35b2 Merge branch 'mk-sbuild-not-automatic' 2023-04-12 09:45:15 -07:00
Steve Langasek
53fcd577e8 We no longer need to run sed 2023-04-12 09:41:52 -07:00
Steve Langasek
8430d445d8 Align with the Launchpad buildd implementation, per review comments 2023-04-12 09:28:41 -07:00
Benjamin Drung
c8a757eb07 Format Python code with black 23.1
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-04-04 12:11:36 +02:00
Nathan Rennie-Waldock
66a2773c1c backportpackage: Fix incorrectly reporting unknown distribution for Ubuntu
Fix incorrectly reporting unknown distribution for Ubuntu after commit
7fc6788b35d32aeb96c7cf81303853d4f31028d1 ("backportpackage: fix
automatic selection of the target release").

LP: #2013237
Signed-off-by: Nathan Rennie-Waldock <nathan.renniewaldock@gmail.com>
2023-04-04 11:50:41 +02:00
Stefano Rivera
17d2770451 Upload to unstable 2023-02-25 13:20:04 -04:00
Stefano Rivera
3136541ca6 Don't run linters at build time, or in autopkgtests. (Closes: #1031436). 2023-02-25 12:52:39 -04:00
Benjamin Drung
f3a0182e1a Release ubuntu-dev-tools 0.192
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-02-01 12:45:31 +01:00
Benjamin Drung
6498a13f18 Drop unneeded X-Python3-Version from d/control
lintain says: "Your sources request a specific set of Python versions
via the control field X-Python3-Version but all declared autopkgtests
exercise all supported Python versions by using the command py3versions
--supported."

Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-02-01 12:43:00 +01:00
Benjamin Drung
d2debf9ed9 Update year in debian/copyright
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-02-01 12:40:33 +01:00
Benjamin Drung
a11cb1f630 Bump Standards-Version to 4.6.2
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-02-01 12:39:50 +01:00
Benjamin Drung
34578e6a1e Enable more pylint checks
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-02-01 12:07:19 +01:00
Benjamin Drung
21784052ba test: Fix deprecated return value for test case
```
ubuntutools/test/test_archive.py::LocalSourcePackageTestCase::test_pull
  /usr/lib/python3.11/unittest/case.py:678: DeprecationWarning: It is deprecated to return a value that is not None from a test case (<bound method LocalSourcePackageTestCase.test_pull of <ubuntutools.test.test_archive.LocalSourcePackageTestCase testMethod=test_pull>>)
    return self.run(*args, **kwds)
```

`test_pull` does not need to be run directly. Make it private.

Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-31 17:39:12 +01:00
Benjamin Drung
aa556af89d Use f-strings
pylint complains about C0209: Formatting a regular string which could be
a f-string (consider-using-f-string)

Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-31 19:32:58 +01:00
Benjamin Drung
069a6926c0 Implement conventions found by pylint
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-31 17:28:33 +01:00
Benjamin Drung
444b319c12 Implement refactorings found by pylint
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-31 16:58:24 +01:00
Benjamin Drung
4449cf2437 Fix warnings found by pylint
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-31 15:51:29 +01:00
Benjamin Drung
9fa29f6ad5 fix(reverse-depends): Restore field titles format
Commit 90e8fe81e1b2610e352c82c0301076ffc7da5ac0 renamed `print_field` to
`log_field`, but changed the `print_field` call with `Logger.info`.
Therefore the line with `=` was lost.

Restore the previous formatting.

Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-31 14:42:22 +01:00
Benjamin Drung
a160def2ab fix(requestbackport): Remove useless loop from locate_package
Commit 0f3d2fed2a4ed67b90b5d49aab25ca2bda5d9d37 removed the difference
between the two loop iterations in `locate_package`. So drop the useless
second iteration.

Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-31 14:35:12 +01:00
Benjamin Drung
909d945af4 Replace deprecated optparse with argparse
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-31 13:33:18 +01:00
Benjamin Drung
f6fde2e217 fix: Use lazy % formatting in logging functions
pylint complains about W1201: Use lazy % formatting in logging functions
(logging-not-lazy) and W1203: Use lazy % formatting in logging functions
(logging-fstring-interpolation).

Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-31 11:13:07 +01:00
Benjamin Drung
17bed46ffb feat: Add some type hints
Add some type hints to satisfy mypy.

Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-31 10:35:22 +01:00
Benjamin Drung
72add78e9d Fix errors found by pylint
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-31 10:19:24 +01:00
Benjamin Drung
ab64467f33 Run pylint during package build again
Commit ae74f71a1e9d4be043162b19d23f2d44c964c771 removed the pylint unit
test saying that unit tests are not needed to just run flake8 or pylint.

Since pylint is useful, add it back, but this time call it directly and
not embed it into a unit test.

Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-31 00:05:15 +01:00
Benjamin Drung
b1bc7e1cdc Address pylint complaints
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-30 23:10:31 +01:00
Benjamin Drung
8692bc2b1c refactor(setup.py): Introduce get_debian_version
Move getting the Debian package version into a separate function and
fail in case it cannot find it or fails parsing it.

Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-30 21:56:37 +01:00
Benjamin Drung
a685368ae9 Run isort import sorter during package build
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-30 21:34:24 +01:00
Benjamin Drung
4e27045f49 style: Sort Python imports with isort
```
isort -l 99 --profile=black .
```

Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-30 21:28:47 +01:00
Benjamin Drung
db0e091e44 Run black code formatter during package build
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-30 19:48:49 +01:00
Benjamin Drung
3354b526b5 style: Format Python code with black
```
PYTHON_SCRIPTS=$(grep -l -r '^#! */usr/bin/python3$' .)
black -C -l 99 . $PYTHON_SCRIPTS
```

Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-30 19:45:36 +01:00
Benjamin Drung
79d24c9df1 fix: Check Python scripts with flake8 again
Commit ae74f71a1e9d4be043162b19d23f2d44c964c771 removed the flake8
unittest and commit 3428a65b1cd644445f55ad8ae65ece5f73d7acb5 added
running flake8 again, but only for files named `*.py`.

Check also all Python scripts with a Python shebang.

Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-30 19:29:51 +01:00
Benjamin Drung
932166484b Fix issues found by flake8 on the Python scripts
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-30 19:29:30 +01:00
Benjamin Drung
bd770fa6b1 test: Do not run flake8 in verbose mode
The verbose output of flake8 is not interesting and just clutters the
output.

Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-30 14:11:09 +01:00
Benjamin Drung
3d54a17403 refactor: Move linter checks into run-linters script
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-30 14:10:02 +01:00
Benjamin Drung
3bdb827516 fix: Use PEP440 compliant version in setup.py
Versions like `0.176ubuntu20.04.1` in Ubuntu are clearly not compliant
with https://peps.python.org/pep-0440/. With setuptools 66, the versions
of all packages visible in the Python environment *must* obey PEP440.

Bug: https://launchpad.net/bugs/1991606
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2023-01-30 14:07:23 +01:00
Mattia Rizzolo
0d94b5e747
document the last commit
Signed-off-by: Mattia Rizzolo <mattia@debian.org>
2023-01-15 18:16:07 +01:00
Krytarik Raido
0f3d2fed2a
requestbackport: Adapt to new backports policy (LP: #1959115)
As documented on <https://wiki.ubuntu.com/UbuntuBackports>

Template update done by Unit 193.

Signed-off-by: Mattia Rizzolo <mattia@debian.org>
2023-01-15 18:14:48 +01:00
Mattia Rizzolo
844d6d942c
Merge branch 'mk-sbuild' of git+ssh://git.launchpad.net/~myamada/ubuntu-dev-tools
Closes: #1001832
LP: #1955116
MR: https://code.launchpad.net/~myamada/ubuntu-dev-tools/+git/ubuntu-dev-tools/+merge/435734
Signed-off-by: Mattia Rizzolo <mattia@debian.org>
2023-01-14 18:49:29 +01:00
Mattia Rizzolo
ae43fd1929
document the previous changes
Signed-off-by: Mattia Rizzolo <mattia@debian.org>
2023-01-14 18:46:50 +01:00
Masahiro Yamada
69ac109cdb mk-sbuild: fix security update repository for Debian bullseye and later
If I run "apt-get update" in the bullseye chroot, I get the following error:

  Err:4 http://security.debian.org bullseye-updates Release
    404  Not Found [IP: 2a04:4e42:600::644 80]

It looks like the directory path was changed since bullseye.

buster:

    deb https://security.debian.org/debian-security buster/updates main

bullseye:

    deb https://security.debian.org/debian-security bullseye-security main

Signed-off-by: Masahiro Yamada <masahiro.yamada@canonical.com>
2023-01-13 18:53:17 +09:00
Masahiro Yamada
9f2a53c166 mk-sbuild: add debian_dist_ge()
Add debian_dist_ge(), which will be used by the next commit.

To avoid code duplication, move the common part to dist_ge().

Signed-off-by: Masahiro Yamada <masahiro.yamada@canonical.com>
2023-01-13 18:34:01 +09:00
Steve Langasek
a69c40d403 Set up preferences for -proposed with NotAutomatic: yes
As of lunar, Ubuntu sets NotAutomatic: yes for its -proposed pockets.  For
sbuild chroots, we want to continue to explicitly install from -proposed by
default; so override with apt preferences to get the correct behavior.
2022-11-16 17:49:13 -08:00
Benjamin Drung
c1e4b14a98 Demote bzr/brz from Recommends to Suggests
Nowadays git is used nearly everywhere. Therefore demoting bzr/brz to
Suggest is the right thing to do.

Bug-Debian: https://bugs.debian.org/940531
Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2022-11-16 18:49:42 +01:00
Benjamin Drung
096d5612e7 sponsor-patch: Use --skip-patches when extract source package
Use `--skip-patches` when extract source packages with `dpkg-source`.
`--no-preparation` is a source package build option and `--skip-patches`
is the correct extract option.

Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2022-11-16 18:37:56 +01:00
Benjamin Drung
b510dbd91e sponsor-patch: Ignore exit code 1 of debdiff call
sponsor-patch calls `debdiff` which exits with 1 if there are
differences. So accept exit codes 0 and 1 as expected.

Signed-off-by: Benjamin Drung <benjamin.drung@canonical.com>
2022-11-15 16:43:27 +01:00
Mattia Rizzolo
803949ed8b
also include a lp bug number there
Signed-off-by: Mattia Rizzolo <mattia@debian.org>
2022-10-11 14:42:08 +02:00
Mattia Rizzolo
e219eaa5fc
Open changelog for the next release.
Signed-off-by: Mattia Rizzolo <mattia@debian.org>
2022-10-11 13:58:00 +02:00
Mattia Rizzolo
60ee986014
Release 0.191
Signed-off-by: Mattia Rizzolo <mattia@debian.org>
2022-10-11 13:56:13 +02:00
Mattia Rizzolo
dabe475067
ubuntutools/archive.py: fix crash in SourcePackage()._source_urls()
Fix operation of SourcePackage._source_urls() (as used, for example, in
SourcePackage.pull() called by backportpackage) to also work when the
class is instantiated with a URL as .dsc.

This is a regression caused by 1b12d8b4e3315de3bf417b40a3c66279f309d72c
(first in v0.184) that moved from os.path.join() to Pathlib, but
os.path.join() was also used to join URLs.

Thanks: Unit 193 for the initial patch.
Signed-off-by: Mattia Rizzolo <mattia@debian.org>
2022-09-29 10:34:51 +02:00
Mattia Rizzolo
0a9e18ed91
document the previous change
Signed-off-by: Mattia Rizzolo <mattia@debian.org>
2022-09-29 10:32:07 +02:00
Stefano Rivera
7859889438 backportpackage: Add support for lsb-release-minimal, which doesn't have a Python module, thanks Gioele Barabucci. (Closes: 1020901) 2022-09-28 11:40:33 +02:00
Gioele Barabucci
a3c87e78aa backportpackage: Run lsb_release as command if the Python module is not available 2022-09-28 11:36:22 +02:00
Mattia Rizzolo
05af489f64
Merge branch 'lp1984113' of git+ssh://git.launchpad.net/~ddstreet/ubuntu-dev-tools
MR: https://code.launchpad.net/~ddstreet/ubuntu-dev-tools/+git/ubuntu-dev-tools/+merge/428101
Signed-off-by: Mattia Rizzolo <mattia@debian.org>
2022-08-22 17:57:07 +02:00
Mattia Rizzolo
d5fdc00396
open changelog for the next release
Signed-off-by: Mattia Rizzolo <mattia@debian.org>
2022-08-22 17:56:02 +02:00
Dan Streetman
7d278cde21 ubuntu-build: use correct exception from LP login failure 2022-08-09 12:15:09 -04:00
Dan Streetman
ad402231db ubuntu-build: explicitly login to LP
LP: #1984113
2022-08-09 12:14:56 -04:00
Dan Streetman
562e6b13cd lpapicache: force lp access on login to workaround possibly invalid cached creds 2022-08-09 12:08:50 -04:00
Dan Streetman
9c1561ff26 lpapicache: remove try-except around login that only logs the error and then re-raises 2022-08-09 12:07:31 -04:00
91 changed files with 6309 additions and 4236 deletions

18
.gitignore vendored
View File

@ -1,16 +1,2 @@
.coverage __pycache__
.tox *.egg-info
/ubuntu_dev_tools.egg-info/
__pycache__/
*.pyc
/build/
/.pybuild/
/test-data/
/debian/python-ubuntutools/
/debian/python3-ubuntutools/
/debian/ubuntu-dev-tools/
/debian/debhelper-build-stamp
/debian/files
/debian/*.debhelper
/debian/*.debhelper.log
/debian/*.substvars

View File

@ -1,5 +1,10 @@
[MASTER] [MASTER]
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list=apt_pkg
# Pickle collected data for later comparisons. # Pickle collected data for later comparisons.
persistent=no persistent=no
@ -9,10 +14,6 @@ jobs=0
[MESSAGES CONTROL] [MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=HIGH
# Disable the message, report, category or checker with the given id(s). You # Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this # can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration # option multiple times (only on the command line, not in the configuration
@ -22,7 +23,18 @@ confidence=HIGH
# --enable=similarities". If you want to run only the classes checker, but have # --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes # no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W" # --disable=W"
disable=locally-disabled disable=fixme,locally-disabled,missing-docstring,useless-option-value,
# TODO: Fix all following disabled checks!
invalid-name,
consider-using-with,
too-many-arguments,
too-many-branches,
too-many-statements,
too-many-locals,
duplicate-code,
too-many-instance-attributes,
too-many-nested-blocks,
too-many-lines,
[REPORTS] [REPORTS]
@ -31,14 +43,6 @@ disable=locally-disabled
reports=no reports=no
[TYPECHECK]
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
# lpapicache classes, urlparse
ignored-classes=Launchpad,BaseWrapper,PersonTeam,Distribution,Consumer,Credentials,ParseResult,apt_pkg,apt_pkg.Dependency,apt_pkg.BaseDependency
[FORMAT] [FORMAT]
# Maximum number of characters on a single line. # Maximum number of characters on a single line.
@ -52,4 +56,10 @@ indent-string=' '
[BASIC] [BASIC]
# Allow variables called e, f, lp # Allow variables called e, f, lp
good-names=i,j,k,ex,Run,_,e,f,lp good-names=i,j,k,ex,Run,_,e,f,lp,me,to
[IMPORTS]
# Force import order to recognize a module as part of a third party library.
known-third-party=debian

View File

@ -18,8 +18,8 @@
# #
# ################################################################## # ##################################################################
import argparse
import glob import glob
import optparse
import os import os
import shutil import shutil
import subprocess import subprocess
@ -27,197 +27,223 @@ import sys
import tempfile import tempfile
from urllib.parse import quote from urllib.parse import quote
import lsb_release try:
from httplib2 import Http, HttpLib2Error import lsb_release
except ImportError:
lsb_release = None
from distro_info import DebianDistroInfo, UbuntuDistroInfo from distro_info import DebianDistroInfo, UbuntuDistroInfo
from httplib2 import Http, HttpLib2Error
from ubuntutools.archive import (DebianSourcePackage,
UbuntuSourcePackage, DownloadError)
from ubuntutools.config import UDTConfig, ubu_email
from ubuntutools.builder import get_builder
from ubuntutools.lp.lpapicache import (Launchpad, Distribution,
SeriesNotFoundException,
PackageNotFoundException)
from ubuntutools.misc import (system_distribution, vendor_to_distroinfo,
codename_to_distribution)
from ubuntutools.question import YesNoQuestion
from ubuntutools import getLogger from ubuntutools import getLogger
from ubuntutools.archive import DebianSourcePackage, DownloadError, UbuntuSourcePackage
from ubuntutools.builder import get_builder
from ubuntutools.config import UDTConfig, ubu_email
from ubuntutools.lp.lpapicache import (
Distribution,
Launchpad,
PackageNotFoundException,
SeriesNotFoundException,
)
from ubuntutools.misc import codename_to_distribution, system_distribution, vendor_to_distroinfo
from ubuntutools.question import YesNoQuestion
Logger = getLogger() Logger = getLogger()
def error(msg): def error(msg, *args):
Logger.error(msg) Logger.error(msg, *args)
sys.exit(1) sys.exit(1)
def check_call(cmd, *args, **kwargs): def check_call(cmd, *args, **kwargs):
Logger.debug(' '.join(cmd)) Logger.debug(" ".join(cmd))
ret = subprocess.call(cmd, *args, **kwargs) ret = subprocess.call(cmd, *args, **kwargs)
if ret != 0: if ret != 0:
error('%s returned %d.' % (cmd[0], ret)) error("%s returned %d.", cmd[0], ret)
def parse(args): def parse(argv):
usage = 'Usage: %prog [options] <source package name or .dsc URL/file>' usage = "%(prog)s [options] <source package name or .dsc URL/file>"
parser = optparse.OptionParser(usage) parser = argparse.ArgumentParser(usage=usage)
parser.add_option('-d', '--destination', parser.add_argument(
metavar='DEST', "-d",
dest='dest_releases', "--destination",
default=[], metavar="DEST",
action='append', dest="dest_releases",
help='Backport to DEST release ' default=[],
'(default: current release)') action="append",
parser.add_option('-s', '--source', help="Backport to DEST release (default: current release)",
metavar='SOURCE', )
dest='source_release', parser.add_argument(
help='Backport from SOURCE release ' "-s",
'(default: devel release)') "--source",
parser.add_option('-S', '--suffix', metavar="SOURCE",
metavar='SUFFIX', dest="source_release",
help='Suffix to append to version number ' help="Backport from SOURCE release (default: devel release)",
'(default: ~ppa1 when uploading to a PPA)') )
parser.add_option('-e', '--message', parser.add_argument(
metavar='MESSAGE', "-S",
default="No-change", "--suffix",
help='Changelog message to use instead of "No-change" ' metavar="SUFFIX",
'(default: No-change backport to DEST.)') help="Suffix to append to version number (default: ~ppa1 when uploading to a PPA)",
parser.add_option('-b', '--build', )
default=False, parser.add_argument(
action='store_true', "-e",
help='Build the package before uploading ' "--message",
'(default: %default)') metavar="MESSAGE",
parser.add_option('-B', '--builder', default="No-change",
metavar='BUILDER', help='Changelog message to use instead of "No-change" '
help='Specify the package builder (default: pbuilder)') "(default: No-change backport to DEST.)",
parser.add_option('-U', '--update', )
default=False, parser.add_argument(
action='store_true', "-b",
help='Update the build environment before ' "--build",
'attempting to build') default=False,
parser.add_option('-u', '--upload', action="store_true",
metavar='UPLOAD', help="Build the package before uploading (default: %(default)s)",
help='Specify an upload destination') )
parser.add_option("-k", "--key", parser.add_argument(
dest='keyid', "-B",
help="Specify the key ID to be used for signing.") "--builder",
parser.add_option('--dont-sign', metavar="BUILDER",
dest='keyid', action='store_false', help="Specify the package builder (default: pbuilder)",
help='Do not sign the upload.') )
parser.add_option('-y', '--yes', parser.add_argument(
dest='prompt', "-U",
default=True, "--update",
action='store_false', default=False,
help='Do not prompt before uploading to a PPA') action="store_true",
parser.add_option('-v', '--version', help="Update the build environment before attempting to build",
metavar='VERSION', )
help='Package version to backport (or verify)') parser.add_argument("-u", "--upload", metavar="UPLOAD", help="Specify an upload destination")
parser.add_option('-w', '--workdir', parser.add_argument(
metavar='WORKDIR', "-k", "--key", dest="keyid", help="Specify the key ID to be used for signing."
help='Specify a working directory ' )
'(default: temporary dir)') parser.add_argument(
parser.add_option('-r', '--release-pocket', "--dont-sign", dest="keyid", action="store_false", help="Do not sign the upload."
default=False, )
action='store_true', parser.add_argument(
help='Target the release pocket in the .changes file. ' "-y",
'Necessary (and default) for uploads to PPAs') "--yes",
parser.add_option('-c', '--close', dest="prompt",
metavar='BUG', default=True,
help='Bug to close in the changelog entry.') action="store_false",
parser.add_option('-m', '--mirror', help="Do not prompt before uploading to a PPA",
metavar='URL', )
help='Preferred mirror (default: Launchpad)') parser.add_argument(
parser.add_option('-l', '--lpinstance', "-v", "--version", metavar="VERSION", help="Package version to backport (or verify)"
metavar='INSTANCE', )
help='Launchpad instance to connect to ' parser.add_argument(
'(default: production)') "-w",
parser.add_option('--no-conf', "--workdir",
default=False, metavar="WORKDIR",
action='store_true', help="Specify a working directory (default: temporary dir)",
help="Don't read config files or environment variables") )
parser.add_argument(
"-r",
"--release-pocket",
default=False,
action="store_true",
help="Target the release pocket in the .changes file. "
"Necessary (and default) for uploads to PPAs",
)
parser.add_argument(
"-c", "--close", metavar="BUG", help="Bug to close in the changelog entry."
)
parser.add_argument(
"-m", "--mirror", metavar="URL", help="Preferred mirror (default: Launchpad)"
)
parser.add_argument(
"-l",
"--lpinstance",
metavar="INSTANCE",
help="Launchpad instance to connect to (default: production)",
)
parser.add_argument(
"--no-conf",
default=False,
action="store_true",
help="Don't read config files or environment variables",
)
parser.add_argument("package_or_dsc", help=argparse.SUPPRESS)
opts, args = parser.parse_args(args) args = parser.parse_args(argv)
if len(args) != 1: config = UDTConfig(args.no_conf)
parser.error('You must specify a single source package or a .dsc ' if args.builder is None:
'URL/path.') args.builder = config.get_value("BUILDER")
config = UDTConfig(opts.no_conf) if not args.update:
if opts.builder is None: args.update = config.get_value("UPDATE_BUILDER", boolean=True)
opts.builder = config.get_value('BUILDER') if args.workdir is None:
if not opts.update: args.workdir = config.get_value("WORKDIR")
opts.update = config.get_value('UPDATE_BUILDER', boolean=True) if args.lpinstance is None:
if opts.workdir is None: args.lpinstance = config.get_value("LPINSTANCE")
opts.workdir = config.get_value('WORKDIR') if args.upload is None:
if opts.lpinstance is None: args.upload = config.get_value("UPLOAD")
opts.lpinstance = config.get_value('LPINSTANCE') if args.keyid is None:
if opts.upload is None: args.keyid = config.get_value("KEYID")
opts.upload = config.get_value('UPLOAD') if not args.upload and not args.workdir:
if opts.keyid is None: parser.error("Please specify either a working dir or an upload target!")
opts.keyid = config.get_value('KEYID') if args.upload and args.upload.startswith("ppa:"):
if not opts.upload and not opts.workdir: args.release_pocket = True
parser.error('Please specify either a working dir or an upload target!')
if opts.upload and opts.upload.startswith('ppa:'):
opts.release_pocket = True
return opts, args, config return args, config
def find_release_package(mirror, workdir, package, version, source_release, def find_release_package(mirror, workdir, package, version, source_release, config):
config):
srcpkg = None srcpkg = None
if source_release: if source_release:
distribution = codename_to_distribution(source_release) distribution = codename_to_distribution(source_release)
if not distribution: if not distribution:
error('Unknown release codename %s' % source_release) error("Unknown release codename %s", source_release)
info = vendor_to_distroinfo(distribution)() info = vendor_to_distroinfo(distribution)()
source_release = info.codename(source_release, default=source_release) source_release = info.codename(source_release, default=source_release)
else: else:
distribution = system_distribution() distribution = system_distribution()
mirrors = [mirror] if mirror else [] mirrors = [mirror] if mirror else []
mirrors.append(config.get_value('%s_MIRROR' % distribution.upper())) mirrors.append(config.get_value(f"{distribution.upper()}_MIRROR"))
if not version: if not version:
archive = Distribution(distribution.lower()).getArchive() archive = Distribution(distribution.lower()).getArchive()
try: try:
spph = archive.getSourcePackage(package, source_release) spph = archive.getSourcePackage(package, source_release)
except (SeriesNotFoundException, PackageNotFoundException) as e: except (SeriesNotFoundException, PackageNotFoundException) as e:
error(str(e)) error("%s", str(e))
version = spph.getVersion() version = spph.getVersion()
if distribution == 'Debian': if distribution == "Debian":
srcpkg = DebianSourcePackage(package, srcpkg = DebianSourcePackage(package, version, workdir=workdir, mirrors=mirrors)
version, elif distribution == "Ubuntu":
workdir=workdir, srcpkg = UbuntuSourcePackage(package, version, workdir=workdir, mirrors=mirrors)
mirrors=mirrors)
elif distribution == 'Ubuntu':
srcpkg = UbuntuSourcePackage(package,
version,
workdir=workdir,
mirrors=mirrors)
return srcpkg return srcpkg
def find_package(mirror, workdir, package, version, source_release, config): def find_package(mirror, workdir, package, version, source_release, config):
"Returns the SourcePackage" "Returns the SourcePackage"
if package.endswith('.dsc'): if package.endswith(".dsc"):
# Here we are using UbuntuSourcePackage just because we don't have any # Here we are using UbuntuSourcePackage just because we don't have any
# "general" class that is safely instantiable (as SourcePackage is an # "general" class that is safely instantiable (as SourcePackage is an
# abstract class). None of the distribution-specific details within # abstract class). None of the distribution-specific details within
# UbuntuSourcePackage is relevant for this use case. # UbuntuSourcePackage is relevant for this use case.
return UbuntuSourcePackage(version=version, dscfile=package, return UbuntuSourcePackage(
workdir=workdir, mirrors=(mirror,)) version=version, dscfile=package, workdir=workdir, mirrors=(mirror,)
)
if not source_release and not version: if not source_release and not version:
info = vendor_to_distroinfo(system_distribution()) info = vendor_to_distroinfo(system_distribution())
source_release = info().devel() source_release = info().devel()
srcpkg = find_release_package(mirror, workdir, package, version, srcpkg = find_release_package(mirror, workdir, package, version, source_release, config)
source_release, config)
if version and srcpkg.version != version: if version and srcpkg.version != version:
error('Requested backport of version %s but version of %s in %s is %s' error(
% (version, package, source_release, srcpkg.version)) "Requested backport of version %s but version of %s in %s is %s",
version,
package,
source_release,
srcpkg.version,
)
return srcpkg return srcpkg
@ -225,30 +251,27 @@ def find_package(mirror, workdir, package, version, source_release, config):
def get_backport_version(version, suffix, upload, release): def get_backport_version(version, suffix, upload, release):
distribution = codename_to_distribution(release) distribution = codename_to_distribution(release)
if not distribution: if not distribution:
error('Unknown release codename %s' % release) error("Unknown release codename %s", release)
if distribution == 'Debian': if distribution == "Debian":
debian_distro_info = DebianDistroInfo() debian_distro_info = DebianDistroInfo()
debian_codenames = debian_distro_info.supported() debian_codenames = debian_distro_info.supported()
if release in debian_codenames: if release in debian_codenames:
release_version = debian_distro_info.version(release) release_version = debian_distro_info.version(release)
if not release_version: if not release_version:
error(f"Can't find the release version for {release}") error("Can't find the release version for %s", release)
backport_version = "{}~bpo{}+1".format( backport_version = f"{version}~bpo{release_version}+1"
version, release_version
)
else: else:
error(f"{release} is not a supported release ({debian_codenames})") error("%s is not a supported release (%s)", release, debian_codenames)
elif distribution == 'Ubuntu': elif distribution == "Ubuntu":
series = Distribution(distribution.lower()).\ series = Distribution(distribution.lower()).getSeries(name_or_version=release)
getSeries(name_or_version=release)
backport_version = version + ('~bpo%s.1' % (series.version)) backport_version = f"{version}~bpo{series.version}.1"
else: else:
error('Unknown distribution «%s» for release «%s»' % (distribution, release)) error("Unknown distribution «%s» for release «%s»", distribution, release)
if suffix is not None: if suffix is not None:
backport_version += suffix backport_version += suffix
elif upload and upload.startswith('ppa:'): elif upload and upload.startswith("ppa:"):
backport_version += '~ppa1' backport_version += "~ppa1"
return backport_version return backport_version
@ -256,26 +279,25 @@ def get_old_version(source, release):
try: try:
distribution = codename_to_distribution(release) distribution = codename_to_distribution(release)
archive = Distribution(distribution.lower()).getArchive() archive = Distribution(distribution.lower()).getArchive()
pkg = archive.getSourcePackage(source, pkg = archive.getSourcePackage(
release, source, release, ("Release", "Security", "Updates", "Proposed", "Backports")
('Release', 'Security', 'Updates', )
'Proposed', 'Backports'))
return pkg.getVersion() return pkg.getVersion()
except (SeriesNotFoundException, PackageNotFoundException): except (SeriesNotFoundException, PackageNotFoundException):
pass pass
return None
def get_backport_dist(release, release_pocket): def get_backport_dist(release, release_pocket):
if release_pocket: if release_pocket:
return release return release
else: return f"{release}-backports"
return '%s-backports' % release
def do_build(workdir, dsc, release, builder, update): def do_build(workdir, dsc, release, builder, update):
builder = get_builder(builder) builder = get_builder(builder)
if not builder: if not builder:
return return None
if update: if update:
if 0 != builder.update(release): if 0 != builder.update(release):
@ -283,41 +305,41 @@ def do_build(workdir, dsc, release, builder, update):
# builder.build is going to chdir to buildresult: # builder.build is going to chdir to buildresult:
workdir = os.path.realpath(workdir) workdir = os.path.realpath(workdir)
return builder.build(os.path.join(workdir, dsc), return builder.build(os.path.join(workdir, dsc), release, os.path.join(workdir, "buildresult"))
release,
os.path.join(workdir, "buildresult"))
def do_upload(workdir, package, bp_version, changes, upload, prompt): def do_upload(workdir, package, bp_version, changes, upload, prompt):
print('Please check %s %s in file://%s carefully!' % (package, bp_version, workdir)) print(f"Please check {package} {bp_version} in file://{workdir} carefully!")
if prompt or upload == 'ubuntu': if prompt or upload == "ubuntu":
question = 'Do you want to upload the package to %s' % upload question = f"Do you want to upload the package to {upload}"
answer = YesNoQuestion().ask(question, "yes") answer = YesNoQuestion().ask(question, "yes")
if answer == "no": if answer == "no":
return return
check_call(['dput', upload, changes], cwd=workdir) check_call(["dput", upload, changes], cwd=workdir)
def orig_needed(upload, workdir, pkg): def orig_needed(upload, workdir, pkg):
'''Avoid a -sa if possible''' """Avoid a -sa if possible"""
if not upload or not upload.startswith('ppa:'): if not upload or not upload.startswith("ppa:"):
return True return True
ppa = upload.split(':', 1)[1] ppa = upload.split(":", 1)[1]
user, ppa = ppa.split('/', 1) user, ppa = ppa.split("/", 1)
version = pkg.version.upstream_version version = pkg.version.upstream_version
h = Http() http = Http()
for filename in glob.glob(os.path.join(workdir, '%s_%s.orig*' % (pkg.source, version))): for filename in glob.glob(os.path.join(workdir, f"{pkg.source}_{version}.orig*")):
url = ('https://launchpad.net/~%s/+archive/%s/+sourcefiles/%s/%s/%s' url = (
% (quote(user), quote(ppa), quote(pkg.source), f"https://launchpad.net/~{quote(user)}/+archive/{quote(ppa)}/+sourcefiles"
quote(pkg.version.full_version), f"/{quote(pkg.source)}/{quote(pkg.version.full_version)}"
quote(os.path.basename(filename)))) f"/{quote(os.path.basename(filename))}"
)
try: try:
headers, body = h.request(url, 'HEAD') headers = http.request(url, "HEAD")[0]
if (headers.status != 200 or if headers.status != 200 or not headers["content-location"].startswith(
not headers['content-location'].startswith('https://launchpadlibrarian.net')): "https://launchpadlibrarian.net"
):
return True return True
except HttpLib2Error as e: except HttpLib2Error as e:
Logger.debug(e) Logger.debug(e)
@ -325,61 +347,79 @@ def orig_needed(upload, workdir, pkg):
return False return False
def do_backport(workdir, pkg, suffix, message, close, release, release_pocket, def do_backport(
build, builder, update, upload, keyid, prompt): workdir,
dirname = '%s-%s' % (pkg.source, release) pkg,
suffix,
message,
close,
release,
release_pocket,
build,
builder,
update,
upload,
keyid,
prompt,
):
dirname = f"{pkg.source}-{release}"
srcdir = os.path.join(workdir, dirname) srcdir = os.path.join(workdir, dirname)
if os.path.exists(srcdir): if os.path.exists(srcdir):
question = 'Working directory %s already exists. Delete it?' % srcdir question = f"Working directory {srcdir} already exists. Delete it?"
if YesNoQuestion().ask(question, 'no') == 'no': if YesNoQuestion().ask(question, "no") == "no":
sys.exit(1) sys.exit(1)
shutil.rmtree(srcdir) shutil.rmtree(srcdir)
pkg.unpack(dirname) pkg.unpack(dirname)
bp_version = get_backport_version(pkg.version.full_version, suffix, bp_version = get_backport_version(pkg.version.full_version, suffix, upload, release)
upload, release)
old_version = get_old_version(pkg.source, release) old_version = get_old_version(pkg.source, release)
bp_dist = get_backport_dist(release, release_pocket) bp_dist = get_backport_dist(release, release_pocket)
changelog = '%s backport to %s.' % (message, release,) changelog = f"{message} backport to {release}."
if close: if close:
changelog += ' (LP: #%s)' % (close,) changelog += f" (LP: #{close})"
check_call(['dch', check_call(
'--force-bad-version', [
'--force-distribution', "dch",
'--preserve', "--force-bad-version",
'--newversion', bp_version, "--force-distribution",
'--distribution', bp_dist, "--preserve",
changelog], "--newversion",
cwd=srcdir) bp_version,
"--distribution",
bp_dist,
changelog,
],
cwd=srcdir,
)
cmd = ['debuild', '--no-lintian', '-S', '-nc', '-uc', '-us'] cmd = ["debuild", "--no-lintian", "-S", "-nc", "-uc", "-us"]
if orig_needed(upload, workdir, pkg): if orig_needed(upload, workdir, pkg):
cmd.append('-sa') cmd.append("-sa")
else: else:
cmd.append('-sd') cmd.append("-sd")
if old_version: if old_version:
cmd.append('-v%s' % old_version) cmd.append(f"-v{old_version}")
env = os.environ.copy() env = os.environ.copy()
# An ubuntu.com e-mail address would make dpkg-buildpackage fail if there # An ubuntu.com e-mail address would make dpkg-buildpackage fail if there
# wasn't an Ubuntu maintainer for an ubuntu-versioned package. LP: #1007042 # wasn't an Ubuntu maintainer for an ubuntu-versioned package. LP: #1007042
env.pop('DEBEMAIL', None) env.pop("DEBEMAIL", None)
check_call(cmd, cwd=srcdir, env=env) check_call(cmd, cwd=srcdir, env=env)
fn_base = pkg.source + '_' + bp_version.split(':', 1)[-1] fn_base = pkg.source + "_" + bp_version.split(":", 1)[-1]
changes = fn_base + '_source.changes' changes = fn_base + "_source.changes"
if build: if build:
if 0 != do_build(workdir, fn_base + '.dsc', release, builder, update): if 0 != do_build(workdir, fn_base + ".dsc", release, builder, update):
sys.exit(1) sys.exit(1)
# None: sign with the default signature. False: don't sign # None: sign with the default signature. False: don't sign
if keyid is not False: if keyid is not False:
cmd = ['debsign'] cmd = ["debsign"]
if keyid: if keyid:
cmd.append('-k' + keyid) cmd.append("-k" + keyid)
cmd.append(changes) cmd.append(changes)
check_call(cmd, cwd=workdir) check_call(cmd, cwd=workdir)
if upload: if upload:
@ -388,63 +428,68 @@ def do_backport(workdir, pkg, suffix, message, close, release, release_pocket,
shutil.rmtree(srcdir) shutil.rmtree(srcdir)
def main(args): def main(argv):
ubu_email() ubu_email()
opts, (package_or_dsc,), config = parse(args[1:]) args, config = parse(argv[1:])
Launchpad.login_anonymously(service=opts.lpinstance) Launchpad.login_anonymously(service=args.lpinstance)
if not opts.dest_releases: if not args.dest_releases:
distinfo = lsb_release.get_distro_information() if lsb_release:
try: distinfo = lsb_release.get_distro_information()
current_distro = distinfo['ID'] try:
except KeyError: current_distro = distinfo["ID"]
error('No destination release specified and unable to guess yours.') except KeyError:
if current_distro == "Ubuntu": error("No destination release specified and unable to guess yours.")
opts.dest_releases = [UbuntuDistroInfo().lts()]
if current_distro == "Debian":
opts.dest_releases = [DebianDistroInfo().stable()]
else: else:
error(f"Unknown distribution {current_distro}, can't guess target release") err, current_distro = subprocess.getstatusoutput("lsb_release --id --short")
if err:
error("Could not run lsb_release to retrieve distribution")
if opts.workdir: if current_distro == "Ubuntu":
workdir = os.path.expanduser(opts.workdir) args.dest_releases = [UbuntuDistroInfo().lts()]
elif current_distro == "Debian":
args.dest_releases = [DebianDistroInfo().stable()]
else:
error("Unknown distribution %s, can't guess target release", current_distro)
if args.workdir:
workdir = os.path.expanduser(args.workdir)
else: else:
workdir = tempfile.mkdtemp(prefix='backportpackage-') workdir = tempfile.mkdtemp(prefix="backportpackage-")
if not os.path.exists(workdir): if not os.path.exists(workdir):
os.makedirs(workdir) os.makedirs(workdir)
try: try:
pkg = find_package(opts.mirror, pkg = find_package(
workdir, args.mirror, workdir, args.package_or_dsc, args.version, args.source_release, config
package_or_dsc, )
opts.version,
opts.source_release,
config)
pkg.pull() pkg.pull()
for release in opts.dest_releases: for release in args.dest_releases:
do_backport(workdir, do_backport(
pkg, workdir,
opts.suffix, pkg,
opts.message, args.suffix,
opts.close, args.message,
release, args.close,
opts.release_pocket, release,
opts.build, args.release_pocket,
opts.builder, args.build,
opts.update, args.builder,
opts.upload, args.update,
opts.keyid, args.upload,
opts.prompt) args.keyid,
args.prompt,
)
except DownloadError as e: except DownloadError as e:
error(str(e)) error("%s", str(e))
finally: finally:
if not opts.workdir: if not args.workdir:
shutil.rmtree(workdir) shutil.rmtree(workdir)
if __name__ == '__main__': if __name__ == "__main__":
sys.exit(main(sys.argv)) sys.exit(main(sys.argv))

View File

@ -36,7 +36,7 @@ _pbuilder-dist()
for distro in $(ubuntu-distro-info --all; debian-distro-info --all) stable testing unstable; do for distro in $(ubuntu-distro-info --all; debian-distro-info --all) stable testing unstable; do
for builder in pbuilder cowbuilder; do for builder in pbuilder cowbuilder; do
echo "$builder-$distro" echo "$builder-$distro"
for arch in i386 amd64 armel armhf; do for arch in i386 amd64 armhf; do
echo "$builder-$distro-$arch" echo "$builder-$distro-$arch"
done done
done done

135
check-mir
View File

@ -21,69 +21,116 @@
# this program; if not, write to the Free Software Foundation, Inc., # this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import sys # pylint: disable=invalid-name
import optparse # pylint: enable=invalid-name
"""Check if any of a package's build or binary dependencies are in universe or multiverse.
Run this inside an unpacked source package
"""
import argparse
import os.path import os.path
import sys
import apt import apt
def check_support(apt_cache, pkgname, alt=False): def check_support(apt_cache, pkgname, alt=False):
'''Check if pkgname is in main or restricted. """Check if pkgname is in main or restricted.
This prints messages if a package is not in main/restricted, or only This prints messages if a package is not in main/restricted, or only
partially (i. e. source in main, but binary in universe). partially (i. e. source in main, but binary in universe).
''' """
if alt: if alt:
prefix = ' ... alternative ' + pkgname prefix = " ... alternative " + pkgname
else: else:
prefix = ' * ' + pkgname prefix = " * " + pkgname
try: prov_packages = apt_cache.get_providing_packages(pkgname)
if pkgname in apt_cache:
pkg = apt_cache[pkgname] pkg = apt_cache[pkgname]
except KeyError:
print(prefix, 'does not exist (pure virtual?)', file=sys.stderr) # If this is a virtual package, iterate through the binary packages that
# provide this, and ensure they are all in Main. Source packages in and of
# themselves cannot provide virtual packages, only binary packages can.
elif len(prov_packages) > 0:
supported, unsupported = [], []
for pkg in prov_packages:
candidate = pkg.candidate
if candidate:
section = candidate.section
if section.startswith("universe") or section.startswith("multiverse"):
unsupported.append(pkg.name)
else:
supported.append(pkg.name)
if len(supported) > 0:
msg = "is a virtual package, which is provided by the following "
msg += "candidates in Main: " + " ".join(supported)
print(prefix, msg)
elif len(unsupported) > 0:
msg = "is a virtual package, but is only provided by the "
msg += "following non-Main candidates: " + " ".join(unsupported)
print(prefix, msg, file=sys.stderr)
return False
else:
msg = "is a virtual package that exists but is not provided by "
msg += "package currently in the archive. Proceed with caution."
print(prefix, msg, file=sys.stderr)
return False
else:
print(prefix, "does not exist", file=sys.stderr)
return False return False
section = pkg.candidate.section section = pkg.candidate.section
if section.startswith('universe') or section.startswith('multiverse'): if section.startswith("universe") or section.startswith("multiverse"):
# check if the source package is in main and thus will only need binary # check if the source package is in main and thus will only need binary
# promotion # promotion
source_records = apt.apt_pkg.SourceRecords() source_records = apt.apt_pkg.SourceRecords()
if not source_records.lookup(pkg.candidate.source_name): if not source_records.lookup(pkg.candidate.source_name):
print('ERROR: Cannot lookup source package for', pkg.name, print("ERROR: Cannot lookup source package for", pkg.name, file=sys.stderr)
file=sys.stderr) print(prefix, "package is in", section.split("/")[0])
print(prefix, 'package is in', section.split('/')[0])
return False return False
src = apt.apt_pkg.TagSection(source_records.record) src = apt.apt_pkg.TagSection(source_records.record)
if (src['Section'].startswith('universe') or if src["Section"].startswith("universe") or src["Section"].startswith("multiverse"):
src['Section'].startswith('multiverse')): print(prefix, "binary and source package is in", section.split("/")[0])
print(prefix, 'binary and source package is in',
section.split('/')[0])
return False return False
else:
print(prefix, 'is in', section.split('/')[0] + ', but its source', print(
pkg.candidate.source_name, prefix,
'is already in main; file an ubuntu-archive bug for ' "is in",
'promoting the current preferred alternative') section.split("/")[0] + ", but its source",
return True pkg.candidate.source_name,
"is already in main; file an ubuntu-archive bug for "
"promoting the current preferred alternative",
)
return True
if alt: if alt:
print(prefix, 'is already in main; consider preferring it') print(prefix, "is already in main; consider preferring it")
return True return True
def check_build_dependencies(apt_cache, control): def check_build_dependencies(apt_cache, control):
print('Checking support status of build dependencies...') print("Checking support status of build dependencies...")
any_unsupported = False any_unsupported = False
for field in ('Build-Depends', 'Build-Depends-Indep'): for field in ("Build-Depends", "Build-Depends-Indep"):
if field not in control.section: if field not in control.section:
continue continue
for or_group in apt.apt_pkg.parse_src_depends(control.section[field]): for or_group in apt.apt_pkg.parse_src_depends(control.section[field]):
pkgname = or_group[0][0] pkgname = or_group[0][0]
# debhelper-compat is expected to be a build dependency of every
# package, so it is a red herring to display it in this report.
# (src:debhelper is in Ubuntu Main anyway)
if pkgname == "debhelper-compat":
continue
if not check_support(apt_cache, pkgname): if not check_support(apt_cache, pkgname):
# check non-preferred alternatives # check non-preferred alternatives
for altpkg in or_group[1:]: for altpkg in or_group[1:]:
@ -98,20 +145,19 @@ def check_build_dependencies(apt_cache, control):
def check_binary_dependencies(apt_cache, control): def check_binary_dependencies(apt_cache, control):
any_unsupported = False any_unsupported = False
print('\nChecking support status of binary dependencies...') print("\nChecking support status of binary dependencies...")
while True: while True:
try: try:
next(control) next(control)
except StopIteration: except StopIteration:
break break
for field in ('Depends', 'Pre-Depends', 'Recommends'): for field in ("Depends", "Pre-Depends", "Recommends"):
if field not in control.section: if field not in control.section:
continue continue
for or_group in apt.apt_pkg.parse_src_depends( for or_group in apt.apt_pkg.parse_src_depends(control.section[field]):
control.section[field]):
pkgname = or_group[0][0] pkgname = or_group[0][0]
if pkgname.startswith('$'): if pkgname.startswith("$"):
continue continue
if not check_support(apt_cache, pkgname): if not check_support(apt_cache, pkgname):
# check non-preferred alternatives # check non-preferred alternatives
@ -125,32 +171,33 @@ def check_binary_dependencies(apt_cache, control):
def main(): def main():
description = "Check if any of a package's build or binary " + \ parser = argparse.ArgumentParser(description=__doc__)
"dependencies are in universe or multiverse. " + \
"Run this inside an unpacked source package"
parser = optparse.OptionParser(description=description)
parser.parse_args() parser.parse_args()
apt_cache = apt.Cache() apt_cache = apt.Cache()
if not os.path.exists('debian/control'): if not os.path.exists("debian/control"):
print('debian/control not found. You need to run this tool in a ' print(
'source package directory', file=sys.stderr) "debian/control not found. You need to run this tool in a source package directory",
file=sys.stderr,
)
sys.exit(1) sys.exit(1)
# get build dependencies from debian/control # get build dependencies from debian/control
control = apt.apt_pkg.TagFile(open('debian/control')) control = apt.apt_pkg.TagFile(open("debian/control", encoding="utf-8"))
next(control) next(control)
unsupported_build_deps = check_build_dependencies(apt_cache, control) unsupported_build_deps = check_build_dependencies(apt_cache, control)
unsupported_binary_deps = check_binary_dependencies(apt_cache, control) unsupported_binary_deps = check_binary_dependencies(apt_cache, control)
if unsupported_build_deps or unsupported_binary_deps: if unsupported_build_deps or unsupported_binary_deps:
print('\nPlease check https://wiki.ubuntu.com/MainInclusionProcess if ' print(
'this source package needs to get into in main/restricted, or ' "\nPlease check https://wiki.ubuntu.com/MainInclusionProcess if "
'reconsider if the package really needs above dependencies.') "this source package needs to get into in main/restricted, or "
"reconsider if the package really needs above dependencies."
)
else: else:
print('All dependencies are supported in main or restricted.') print("All dependencies are supported in main or restricted.")
if __name__ == '__main__': if __name__ == "__main__":
main() main()

1
debian/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
files

264
debian/changelog vendored
View File

@ -1,3 +1,267 @@
ubuntu-dev-tools (0.206) unstable; urgency=medium
[ Dan Bungert ]
* mk-sbuild: enable pkgmaintainermangler
[ Shengjing Zhu ]
* import-bug-from-debian: package option is overridden and not used
[ Fernando Bravo Hernández ]
* Parsing arch parameter to getBinaryPackage() (LP: #2081861)
[ Simon Quigley ]
* Read ~/.devscripts in a more robust way, to ideally pick up multi-line
variables (Closes: #725418).
* mk-sbuild: default to using UTC for schroots (LP: #2097159).
* syncpackage: s/syncblacklist/syncblocklist/g
* syncpackage: Cache the sync blocklist in-memory, so it's not fetched
multiple times when syncing more than one package.
* syncpackage: Catch exceptions cleanly, simply skipping to the next
package (erring on the side of caution) if there is an error doing the
download (LP: #1943286).
-- Simon Quigley <tsimonq2@debian.org> Tue, 04 Mar 2025 13:43:15 -0600
ubuntu-dev-tools (0.205) unstable; urgency=medium
* [syncpackage] When syncing multiple packages, if one of the packages is in
the sync blocklist, do not exit, simply continue.
* [syncpackage] Do not use exit(1) on an error or exception unless it
applies to all packages, instead return None so we can continue to the
next package.
* [syncpackage] Add support for -y or --yes, noted that it should be used
with care.
* Update Standards-Version to 4.7.2, no changes needed.
-- Simon Quigley <tsimonq2@debian.org> Sat, 01 Mar 2025 11:29:54 -0600
ubuntu-dev-tools (0.204) unstable; urgency=medium
[ Simon Quigley ]
* Update Standards-Version to 4.7.1, no changes needed.
* Add several Lintian overrides related to .pyc files.
* Add my name to the copyright file.
* Rename bitesize to lp-bitesize (Closes: #1076224).
* Add a manpage for running-autopkgtests.
* Add a large warning at the top of mk-sbuild encouraging the use of the
unshare backend. This is to provide ample warning to users.
* Remove mail line from default ~/.sbuildrc, to resolve the undeclared
dependency on sendmail (Closes: #1074632).
[ Julien Plissonneau Duquène ]
* Fix reverse-depends -b crash on packages that b-d on themselves
(Closes: #1087760).
-- Simon Quigley <tsimonq2@debian.org> Mon, 24 Feb 2025 19:54:39 -0600
ubuntu-dev-tools (0.203) unstable; urgency=medium
[ Steve Langasek ]
* ubuntu-build: handle TOCTOU issue with the "can be retried" value on
builds.
* Recommend sbuild over pbuilder. sbuild is the tool recommended by
Ubuntu developers whose behavior most closely approximates Launchpad
builds.
[ Florent 'Skia' Jacquet ]
* import-bug-from-debian: handle multipart message (Closes: #969510)
[ Benjamin Drung ]
* import-bug-from-debian: add type hints
* Bump Standards-Version to 4.7.0
* Bump year and add missing files to copyright
* setup.py: add pm-helper
* Format code with black and isort
* Address several issues pointed out by Pylint
* Depend on python3-yaml for pm-helper
-- Benjamin Drung <bdrung@debian.org> Sat, 02 Nov 2024 18:19:24 +0100
ubuntu-dev-tools (0.202) unstable; urgency=medium
[ Steve Langasek ]
* ubuntu-build: support --batch with no package names to retry all
* ubuntu-build: in batch mode, print a count of packages retried
* ubuntu-build: make the --arch option top-level.
This gets rid of the fugly --arch2 option
* ubuntu-build: support retrying builds in other states that failed-to-build
* ubuntu-build: Handling of proposed vs release pocket default for ppas
* ubuntu-build: update manpage
[ Chris Peterson ]
* Replace Depends on python3-launchpadlib with Depends on
python3-launchpadlib-desktop (LP: #2049217)
-- Simon Quigley <tsimonq2@ubuntu.com> Fri, 12 Apr 2024 23:33:14 -0500
ubuntu-dev-tools (0.201) unstable; urgency=medium
* running-autopkgtests: fix packaging to make the script available
(LP: #2055466)
-- Chris Peterson <chris.peterson@canonical.com> Thu, 29 Feb 2024 11:09:14 -0800
ubuntu-dev-tools (0.200) unstable; urgency=medium
[ Gianfranco Costamagna ]
* Team upload
[ Chris Peterson ]
* Add support to see currently running autopkgtests (running-autopkgtests)
* running-autopkgtests: use f-strings
[ Athos Ribeiro ]
* syncpackage: log LP authentication errors before halting.
[ Ying-Chun Liu (PaulLiu) ]
* Drop qemu-debootstrap
qemu-debootstrap is deprecated for a while. In newer qemu release
the command is totally removed. We can use debootstrap directly.
Signed-off-by: Ying-Chun Liu (PaulLiu) <paulliu@debian.org>
[ Logan Rosen ]
* Don't rely on debootstrap for validating Ubuntu distro
-- Gianfranco Costamagna <locutusofborg@debian.org> Thu, 15 Feb 2024 17:53:48 +0100
ubuntu-dev-tools (0.199) unstable; urgency=medium
[ Simon Quigley ]
* Add my name to Uploaders.
[ Steve Langasek ]
* Introduce a pm-helper tool.
-- Simon Quigley <tsimonq2@debian.org> Mon, 29 Jan 2024 10:03:22 -0600
ubuntu-dev-tools (0.198) unstable; urgency=medium
* In check-mir, ignore debhelper-compat when checking the build
dependencies. This is expected to be a build dependency of all packages,
so warning about it in any way is surely a red herring.
* Add proper support for virtual packages in check-mir, basing the
determination solely off of binary packages. This is not expected to be a
typical case.
-- Simon Quigley <tsimonq2@debian.org> Wed, 10 Jan 2024 20:04:02 -0600
ubuntu-dev-tools (0.197) unstable; urgency=medium
* Update the manpage for syncpackage to reflect the ability to sync
multiple packages at once.
* When using pull-*-source to grab a package which already has a defined
Vcs- field, display the exact same warning message `apt source` does.
-- Simon Quigley <tsimonq2@debian.org> Tue, 03 Oct 2023 14:01:25 -0500
ubuntu-dev-tools (0.196) unstable; urgency=medium
* Allow the user to sync multiple packages at one time (LP: #1756748).
-- Simon Quigley <tsimonq2@debian.org> Fri, 04 Aug 2023 14:37:59 -0500
ubuntu-dev-tools (0.195) unstable; urgency=medium
* Add support for the non-free-firmware components in all tools already
referencing non-free.
-- Simon Quigley <tsimonq2@debian.org> Wed, 26 Jul 2023 13:03:31 -0500
ubuntu-dev-tools (0.194) unstable; urgency=medium
[ Gianfranco Costamagna ]
* ubuntu-build: For some reasons, now you need to be authenticated before
trying to use the "PersonTeam" class features.
Do it at the begin instead of replicating the same code inside the
tool itself.
[ Steve Langasek ]
* Remove references to deprecated
http://people.canonical.com/~ubuntu-archive.
* Remove references to architectures not supported in any active
Ubuntu release.
* Remove references to ftpmaster.internal. When this name is resolvable
but firewalled, syncpackage hangs; and these are tools for developers,
not for running in an automated context in the DCs where
ftpmaster.internal is reachable.
* Excise all references to cdbs (including in test cases)
* Set apt preferences for the -proposed pocket in mk-sbuild so that
it works as expected for lunar and forward.
[ Robie Basak ]
* ubuntutools/misc: swap iter_content for raw stream with "Accept-Encoding:
identity" to fix .diff.gz downloads (LP: #2025748).
[ Vladimir Petko ]
* Fix a typo introduced in the last upload that made mk-sbuild fail
unconditionally. LP: #2017177.
-- Gianfranco Costamagna <locutusofborg@debian.org> Sat, 08 Jul 2023 08:42:05 +0200
ubuntu-dev-tools (0.193) unstable; urgency=medium
* Don't run linters at build time, or in autopkgtests. (Closes: #1031436).
-- Stefano Rivera <stefanor@debian.org> Sat, 25 Feb 2023 13:19:56 -0400
ubuntu-dev-tools (0.192) unstable; urgency=medium
[ Benjamin Drung ]
* sponsor-patch:
+ Ignore exit code 1 of debdiff call.
+ Use --skip-patches instead of --no-preparation with dpkg-source -x.
* Demote bzr/brz from Recommends to Suggests, as nowadays git is the way.
Closes: #940531
* Use PEP440 compliant version in setup.py (LP: #1991606)
* Fix issues found by flake8 on the Python scripts
* Check Python scripts with flake8 again
* Format Python code with black and run black during package build
* Sort Python imports with isort and run isort during package build
* Replace deprecated optparse with argparse
* requestbackport: Remove useless loop from locate_package
* reverse-depends: Restore field titles format
* test: Fix deprecated return value for test case
* Fix all errors and warnings found by pylint and implement most refactorings
and conventions. Run pylint during package build again.
* Bump Standards-Version to 4.6.2
* Drop unneeded X-Python3-Version from d/control
[ Masahiro Yamada ]
* mk-sbuild:
+ Handle the new location of the Debian bullseye security archive.
Closes: #1001832; LP: #1955116
[ Mattia Rizzolo ]
* requestbackport:
+ Apply patch from Krytarik Raido and Unit 193 to update the template and
workflow after the new Ubuntu Backport process has been established.
LP: #1959115
-- Benjamin Drung <bdrung@debian.org> Wed, 01 Feb 2023 12:45:15 +0100
ubuntu-dev-tools (0.191) unstable; urgency=medium
[ Dan Streetman ]
* lpapicache:
+ Make sure that login() actually logins and doesn't use cached credentials.
* ubuntu-build:
+ Fix crash caused by a change in lpapicache that changed the default
operation mode from authenticated to anonymous. LP: #1984113
[ Stefano Rivera ]
* backportpackage:
+ Add support for lsb-release-minimal, which doesn't have a Python module.
Thanks to Gioele Barabucci for the patch. Closes: #1020901; LP: #1991828
[ Mattia Rizzolo ]
* ubuntutools/archive.py:
+ Fix operation of SourcePackage._source_urls() (as used, for example, in
SourcePackage.pull() called by backportpackage) to also work when the
class is instantiated with a URL as .dsc. Fixes regression from v0.184.
Thanks to Unit 193 for the initial patch.
-- Mattia Rizzolo <mattia@debian.org> Tue, 11 Oct 2022 13:56:03 +0200
ubuntu-dev-tools (0.190) unstable; urgency=medium ubuntu-dev-tools (0.190) unstable; urgency=medium
[ Dimitri John Ledkov ] [ Dimitri John Ledkov ]

25
debian/control vendored
View File

@ -6,7 +6,9 @@ Uploaders:
Benjamin Drung <bdrung@debian.org>, Benjamin Drung <bdrung@debian.org>,
Stefano Rivera <stefanor@debian.org>, Stefano Rivera <stefanor@debian.org>,
Mattia Rizzolo <mattia@debian.org>, Mattia Rizzolo <mattia@debian.org>,
Simon Quigley <tsimonq2@debian.org>,
Build-Depends: Build-Depends:
black <!nocheck>,
dctrl-tools, dctrl-tools,
debhelper-compat (= 13), debhelper-compat (= 13),
devscripts (>= 2.11.0~), devscripts (>= 2.11.0~),
@ -14,23 +16,26 @@ Build-Depends:
dh-python, dh-python,
distro-info (>= 0.2~), distro-info (>= 0.2~),
flake8, flake8,
isort <!nocheck>,
lsb-release, lsb-release,
pylint <!nocheck>,
python3-all, python3-all,
python3-apt, python3-apt,
python3-dateutil,
python3-debian, python3-debian,
python3-debianbts, python3-debianbts,
python3-distro-info, python3-distro-info,
python3-httplib2, python3-httplib2,
python3-launchpadlib, python3-launchpadlib-desktop,
python3-pytest, python3-pytest,
python3-requests <!nocheck>, python3-requests <!nocheck>,
python3-setuptools, python3-setuptools,
Standards-Version: 4.6.1 python3-yaml <!nocheck>,
Standards-Version: 4.7.2
Rules-Requires-Root: no Rules-Requires-Root: no
Vcs-Git: https://git.launchpad.net/ubuntu-dev-tools Vcs-Git: https://git.launchpad.net/ubuntu-dev-tools
Vcs-Browser: https://git.launchpad.net/ubuntu-dev-tools Vcs-Browser: https://git.launchpad.net/ubuntu-dev-tools
Homepage: https://launchpad.net/ubuntu-dev-tools Homepage: https://launchpad.net/ubuntu-dev-tools
X-Python3-Version: >= 3.6
Package: ubuntu-dev-tools Package: ubuntu-dev-tools
Architecture: all Architecture: all
@ -49,9 +54,10 @@ Depends:
python3-debianbts, python3-debianbts,
python3-distro-info, python3-distro-info,
python3-httplib2, python3-httplib2,
python3-launchpadlib, python3-launchpadlib-desktop,
python3-lazr.restfulclient, python3-lazr.restfulclient,
python3-ubuntutools (= ${binary:Version}), python3-ubuntutools (= ${binary:Version}),
python3-yaml,
sensible-utils, sensible-utils,
sudo, sudo,
tzdata, tzdata,
@ -59,8 +65,6 @@ Depends:
${perl:Depends}, ${perl:Depends},
Recommends: Recommends:
arch-test, arch-test,
bzr | brz,
bzr-builddeb | brz-debian,
ca-certificates, ca-certificates,
debian-archive-keyring, debian-archive-keyring,
debian-keyring, debian-keyring,
@ -68,12 +72,14 @@ Recommends:
genisoimage, genisoimage,
lintian, lintian,
patch, patch,
pbuilder | cowbuilder | sbuild, sbuild | pbuilder | cowbuilder,
python3-dns, python3-dns,
quilt, quilt,
reportbug (>= 3.39ubuntu1), reportbug (>= 3.39ubuntu1),
ubuntu-keyring | ubuntu-archive-keyring, ubuntu-keyring | ubuntu-archive-keyring,
Suggests: Suggests:
bzr | brz,
bzr-builddeb | brz-debian,
qemu-user-static, qemu-user-static,
Description: useful tools for Ubuntu developers Description: useful tools for Ubuntu developers
This is a collection of useful tools that Ubuntu developers use to make their This is a collection of useful tools that Ubuntu developers use to make their
@ -112,6 +118,8 @@ Description: useful tools for Ubuntu developers
- requestsync - files a sync request with Debian changelog and rationale. - requestsync - files a sync request with Debian changelog and rationale.
- reverse-depends - find the reverse dependencies (or build dependencies) of - reverse-depends - find the reverse dependencies (or build dependencies) of
a package. a package.
- running-autopkgtests - lists the currently running and/or queued
autopkgtests on the Ubuntu autopkgtest infrastructure
- seeded-in-ubuntu - query if a package is safe to upload during a freeze. - seeded-in-ubuntu - query if a package is safe to upload during a freeze.
- setup-packaging-environment - assistant to get an Ubuntu installation - setup-packaging-environment - assistant to get an Ubuntu installation
ready for packaging work. ready for packaging work.
@ -130,10 +138,11 @@ Package: python3-ubuntutools
Architecture: all Architecture: all
Section: python Section: python
Depends: Depends:
python3-dateutil,
python3-debian, python3-debian,
python3-distro-info, python3-distro-info,
python3-httplib2, python3-httplib2,
python3-launchpadlib, python3-launchpadlib-desktop,
python3-lazr.restfulclient, python3-lazr.restfulclient,
python3-requests, python3-requests,
sensible-utils, sensible-utils,

25
debian/copyright vendored
View File

@ -11,6 +11,7 @@ Files: backportpackage
doc/check-symbols.1 doc/check-symbols.1
doc/requestsync.1 doc/requestsync.1
doc/ubuntu-iso.1 doc/ubuntu-iso.1
doc/running-autopkgtests.1
GPL-2 GPL-2
README.updates README.updates
requestsync requestsync
@ -19,12 +20,13 @@ Files: backportpackage
ubuntu-iso ubuntu-iso
ubuntutools/requestsync/*.py ubuntutools/requestsync/*.py
Copyright: 2007, Albert Damen <albrt@gmx.net> Copyright: 2007, Albert Damen <albrt@gmx.net>
2010-2022, Benjamin Drung <bdrung@ubuntu.com> 2010-2024, Benjamin Drung <bdrung@ubuntu.com>
2007-2010, Canonical Ltd. 2007-2023, Canonical Ltd.
2006-2007, Daniel Holbach <daniel.holbach@ubuntu.com> 2006-2007, Daniel Holbach <daniel.holbach@ubuntu.com>
2010, Evan Broder <evan@ebroder.net> 2010, Evan Broder <evan@ebroder.net>
2006-2007, Luke Yelavich <themuso@ubuntu.com> 2006-2007, Luke Yelavich <themuso@ubuntu.com>
2009-2010, Michael Bienia <geser@ubuntu.com> 2009-2010, Michael Bienia <geser@ubuntu.com>
2024-2025, Simon Quigley <tsimonq2@debian.org>
2010-2011, Stefano Rivera <stefanor@ubuntu.com> 2010-2011, Stefano Rivera <stefanor@ubuntu.com>
2008, Stephan Hermann <sh@sourcecode.de> 2008, Stephan Hermann <sh@sourcecode.de>
2007, Steve Kowalik <stevenk@ubuntu.com> 2007, Steve Kowalik <stevenk@ubuntu.com>
@ -72,21 +74,28 @@ License: GPL-2+
On Debian systems, the complete text of the GNU General Public License On Debian systems, the complete text of the GNU General Public License
version 2 can be found in the /usr/share/common-licenses/GPL-2 file. version 2 can be found in the /usr/share/common-licenses/GPL-2 file.
Files: doc/bitesize.1 Files: doc/lp-bitesize.1
doc/check-mir.1 doc/check-mir.1
doc/grab-merge.1 doc/grab-merge.1
doc/merge-changelog.1 doc/merge-changelog.1
doc/pm-helper.1
doc/setup-packaging-environment.1 doc/setup-packaging-environment.1
doc/syncpackage.1 doc/syncpackage.1
bitesize lp-bitesize
check-mir check-mir
GPL-3 GPL-3
grab-merge grab-merge
merge-changelog merge-changelog
pm-helper
pyproject.toml
run-linters
running-autopkgtests
setup-packaging-environment setup-packaging-environment
syncpackage syncpackage
Copyright: 2010, Benjamin Drung <bdrung@ubuntu.com> ubuntutools/running_autopkgtests.py
2007-2011, Canonical Ltd. ubuntutools/utils.py
Copyright: 2010-2024, Benjamin Drung <bdrung@ubuntu.com>
2007-2024, Canonical Ltd.
2008, Jonathan Patrick Davies <jpds@ubuntu.com> 2008, Jonathan Patrick Davies <jpds@ubuntu.com>
2008-2010, Martin Pitt <martin.pitt@canonical.com> 2008-2010, Martin Pitt <martin.pitt@canonical.com>
2009, Siegfried-Angel Gevatter Pujals <rainct@ubuntu.com> 2009, Siegfried-Angel Gevatter Pujals <rainct@ubuntu.com>
@ -174,11 +183,13 @@ Files: doc/pull-debian-debdiff.1
ubuntutools/update_maintainer.py ubuntutools/update_maintainer.py
ubuntutools/version.py ubuntutools/version.py
update-maintainer update-maintainer
Copyright: 2009-2011, Benjamin Drung <bdrung@ubuntu.com> .pylintrc
Copyright: 2009-2024, Benjamin Drung <bdrung@ubuntu.com>
2010, Evan Broder <evan@ebroder.net> 2010, Evan Broder <evan@ebroder.net>
2008, Siegfried-Angel Gevatter Pujals <rainct@ubuntu.com> 2008, Siegfried-Angel Gevatter Pujals <rainct@ubuntu.com>
2010-2011, Stefano Rivera <stefanor@ubuntu.com> 2010-2011, Stefano Rivera <stefanor@ubuntu.com>
2017-2021, Dan Streetman <ddstreet@canonical.com> 2017-2021, Dan Streetman <ddstreet@canonical.com>
2024, Canonical Ltd.
License: ISC License: ISC
Permission to use, copy, modify, and/or distribute this software for any Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above purpose with or without fee is hereby granted, provided that the above

1
debian/rules vendored
View File

@ -7,7 +7,6 @@ override_dh_auto_clean:
override_dh_auto_test: override_dh_auto_test:
ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
flake8 -v --max-line-length=99
python3 -m pytest -v ubuntutools python3 -m pytest -v ubuntutools
endif endif

3
debian/source/lintian-overrides vendored Normal file
View File

@ -0,0 +1,3 @@
# pyc files are machine-generated; they're expected to have long lines and have unstated copyright
source: file-without-copyright-information *.pyc [debian/copyright]
source: very-long-line-length-in-source-file * > 512 [*.pyc:*]

View File

@ -1,8 +1,3 @@
Test-Command: flake8 -v --max-line-length=99
Depends:
flake8,
Restrictions: allow-stderr
Test-Command: python3 -m pytest -v ubuntutools Test-Command: python3 -m pytest -v ubuntutools
Depends: Depends:
dh-make, dh-make,

View File

@ -1,21 +1,21 @@
.TH bitesize "1" "May 9 2010" "ubuntu-dev-tools" .TH lp-bitesize "1" "May 9 2010" "ubuntu-dev-tools"
.SH NAME .SH NAME
bitesize \- Add \fBbitesize\fR tag to bugs and add a comment. lp-bitesize \- Add \fBbitesize\fR tag to bugs and add a comment.
.SH SYNOPSIS .SH SYNOPSIS
.B bitesize \fR<\fIbug number\fR> .B lp-bitesize \fR<\fIbug number\fR>
.br .br
.B bitesize \-\-help .B lp-bitesize \-\-help
.SH DESCRIPTION .SH DESCRIPTION
\fBbitesize\fR adds a bitesize tag to the bug, if it's not there yet. It \fBlp-bitesize\fR adds a bitesize tag to the bug, if it's not there yet. It
also adds a comment to the bug indicating that you are willing to help with also adds a comment to the bug indicating that you are willing to help with
fixing it. fixing it.
It checks for permission to operate on a given bug first, It checks for permission to operate on a given bug first,
then perform required tasks on Launchpad. then perform required tasks on Launchpad.
.SH OPTIONS .SH OPTIONS
Listed below are the command line options for \fBbitesize\fR: Listed below are the command line options for \fBlp-bitesize\fR:
.TP .TP
.BR \-h ", " \-\-help .BR \-h ", " \-\-help
Display a help message and exit. Display a help message and exit.
@ -48,7 +48,7 @@ The default value for \fB--lpinstance\fR.
.BR ubuntu\-dev\-tools (5) .BR ubuntu\-dev\-tools (5)
.SH AUTHORS .SH AUTHORS
\fBbitesize\fR and this manual page were written by Daniel Holbach \fBlp-bitesize\fR and this manual page were written by Daniel Holbach
<daniel.holbach@canonical.com>. <daniel.holbach@canonical.com>.
.PP .PP
Both are released under the terms of the GNU General Public License, version 3. Both are released under the terms of the GNU General Public License, version 3.

View File

@ -20,7 +20,7 @@ like for example \fBpbuilder\-feisty\fP, \fBpbuilder\-sid\fP, \fBpbuilder\-gutsy
.PP .PP
The same applies to \fBcowbuilder\-dist\fP, which uses cowbuilder. The main The same applies to \fBcowbuilder\-dist\fP, which uses cowbuilder. The main
difference between both is that pbuilder compresses the created chroot as a difference between both is that pbuilder compresses the created chroot as a
a tarball, thus using less disc space but needing to uncompress (and possibly tarball, thus using less disc space but needing to uncompress (and possibly
compress) its contents again on each run, and cowbuilder doesn't do this. compress) its contents again on each run, and cowbuilder doesn't do this.
.SH USAGE .SH USAGE
@ -38,7 +38,7 @@ This optional parameter will attempt to construct a chroot in a foreign
architecture. architecture.
For some architecture pairs (e.g. i386 on an amd64 install), the chroot For some architecture pairs (e.g. i386 on an amd64 install), the chroot
will be created natively. will be created natively.
For others (e.g. armel on an i386 install), qemu\-user\-static will be For others (e.g. arm64 on an amd64 install), qemu\-user\-static will be
used. used.
Note that some combinations (e.g. amd64 on an i386 install) require Note that some combinations (e.g. amd64 on an i386 install) require
special separate kernel handling, and may break in unexpected ways. special separate kernel handling, and may break in unexpected ways.

44
doc/pm-helper.1 Normal file
View File

@ -0,0 +1,44 @@
.\" Copyright (C) 2023, Canonical Ltd.
.\"
.\" This program is free software; you can redistribute it and/or
.\" modify it under the terms of the GNU General Public License, version 3.
.\"
.\" This program is distributed in the hope that it will be useful,
.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
.\" General Public License for more details.
.\"
.\" You should have received a copy of the GNU General Public License
.\" along with this program. If not, see <http://www.gnu.org/licenses/>.
.TH pm\-helper 1 "June 2023" ubuntu\-dev\-tools
.SH NAME
pm\-helper \- helper to guide a developer through proposed\-migration work
.SH SYNOPSIS
.B pm\-helper \fR[\fIoptions\fR] [\fIpackage\fR]
.SH DESCRIPTION
Claim a package from proposed\-migration to work on and get additional
information (such as the state of the package in Debian) that may be helpful
in unblocking it.
.PP
This tool is incomplete and under development.
.SH OPTIONS
.TP
.B \-l \fIINSTANCE\fR, \fB\-\-launchpad\fR=\fIINSTANCE\fR
Use the specified instance of Launchpad (e.g. "staging"), instead of
the default of "production".
.TP
.B \-v\fR, \fB--verbose\fR
be more verbose
.TP
\fB\-h\fR, \fB\-\-help\fR
Display a help message and exit
.SH AUTHORS
\fBpm\-helper\fR and this manpage were written by Steve Langasek
<steve.langasek@ubuntu.com>.
.PP
Both are released under the GPLv3 license.

View File

@ -0,0 +1,15 @@
.TH running\-autopkgtests "1" "18 January 2024" "ubuntu-dev-tools"
.SH NAME
running\-autopkgtests \- dumps a list of currently running autopkgtests
.SH SYNOPSIS
.B running\-autopkgtests
.SH DESCRIPTION
Dumps a list of currently running and queued tests in Autopkgtest.
Pass --running to only see running tests, or --queued to only see
queued tests. Passing both will print both, which is the default behavior.
.SH AUTHOR
.B running\-autopkgtests
was written by Chris Peterson <chris.peterson@canonical.com>.

View File

@ -11,7 +11,7 @@ contributors to get their Ubuntu installation ready for packaging work. It
ensures that all four components from Ubuntu's official repositories are enabled ensures that all four components from Ubuntu's official repositories are enabled
along with their corresponding source repositories. It also installs a minimal along with their corresponding source repositories. It also installs a minimal
set of packages needed for Ubuntu packaging work (ubuntu-dev-tools, devscripts, set of packages needed for Ubuntu packaging work (ubuntu-dev-tools, devscripts,
debhelper, cdbs, patchutils, pbuilder, and build-essential). Finally, it assists debhelper, patchutils, pbuilder, and build-essential). Finally, it assists
in defining the DEBEMAIL and DEBFULLNAME environment variables. in defining the DEBEMAIL and DEBFULLNAME environment variables.
.SH AUTHORS .SH AUTHORS

View File

@ -4,11 +4,11 @@ syncpackage \- copy source packages from Debian to Ubuntu
.\" .\"
.SH SYNOPSIS .SH SYNOPSIS
.B syncpackage .B syncpackage
[\fIoptions\fR] \fI<.dsc URL/path or package name>\fR [\fIoptions\fR] \fI<.dsc URL/path or package name(s)>\fR
.\" .\"
.SH DESCRIPTION .SH DESCRIPTION
\fBsyncpackage\fR causes a source package to be copied from Debian to \fBsyncpackage\fR causes one or more source package(s) to be copied from Debian
Ubuntu. to Ubuntu.
.PP .PP
\fBsyncpackage\fR allows you to upload files with the same checksums of the \fBsyncpackage\fR allows you to upload files with the same checksums of the
Debian ones, as the common script used by Ubuntu archive administrators does, Debian ones, as the common script used by Ubuntu archive administrators does,
@ -58,7 +58,7 @@ Display more progress information.
\fB\-F\fR, \fB\-\-fakesync\fR \fB\-F\fR, \fB\-\-fakesync\fR
Perform a fakesync, to work around a tarball mismatch between Debian and Perform a fakesync, to work around a tarball mismatch between Debian and
Ubuntu. Ubuntu.
This option ignores blacklisting, and performs a local sync. This option ignores blocklisting, and performs a local sync.
It implies \fB\-\-no\-lp\fR, and will leave a signed \fB.changes\fR file It implies \fB\-\-no\-lp\fR, and will leave a signed \fB.changes\fR file
for you to upload. for you to upload.
.TP .TP

View File

@ -1,9 +1,14 @@
.TH UBUNTU-BUILD "1" "June 2010" "ubuntu-dev-tools" .TH UBUNTU-BUILD "1" "Mar 2024" "ubuntu-dev-tools"
.SH NAME .SH NAME
ubuntu-build \- command-line interface to Launchpad build operations ubuntu-build \- command-line interface to Launchpad build operations
.SH SYNOPSIS .SH SYNOPSIS
.B ubuntu-build <srcpackage> <release> <operation> .nf
\fBubuntu-build\fR <srcpackage> <release> <operation>
\fBubuntu-build\fR --batch [--retry] [--rescore \fIPRIORITY\fR] [--arch \fIARCH\fR [...]]
[--series \fISERIES\fR] [--state \fIBUILD-STATE\fR]
[-A \fIARCHIVE\fR] [pkg]...
.fi
.SH DESCRIPTION .SH DESCRIPTION
\fBubuntu-build\fR provides a command line interface to the Launchpad build \fBubuntu-build\fR provides a command line interface to the Launchpad build
@ -38,8 +43,7 @@ operations.
\fB\-a\fR ARCHITECTURE, \fB\-\-arch\fR=\fIARCHITECTURE\fR \fB\-a\fR ARCHITECTURE, \fB\-\-arch\fR=\fIARCHITECTURE\fR
Rebuild or rescore a specific architecture. Valid Rebuild or rescore a specific architecture. Valid
architectures are: architectures are:
armel, armhf, arm64, amd64, hppa, i386, ia64, armhf, arm64, amd64, i386, powerpc, ppc64el, riscv64, s390x.
lpia, powerpc, ppc64el, riscv64, s390x, sparc.
.TP .TP
Batch processing: Batch processing:
.IP .IP
@ -59,15 +63,16 @@ Retry builds (give\-back).
\fB\-\-rescore\fR=\fIPRIORITY\fR \fB\-\-rescore\fR=\fIPRIORITY\fR
Rescore builds to <priority>. Rescore builds to <priority>.
.IP .IP
\fB\-\-arch2\fR=\fIARCHITECTURE\fR \fB\-\-arch\fR=\fIARCHITECTURE\fR
Affect only 'architecture' (can be used several Affect only 'architecture' (can be used several
times). Valid architectures are: times). Valid architectures are:
armel, armhf, arm64, amd64, hppa, i386, ia64, arm64, amd64, i386, powerpc, ppc64el, riscv64, s390x.
lpia, powerpc, ppc64el, riscv64, s390x, sparc. .IP
\fB\-A=\fIARCHIVE\fR
Act on the named archive (ppa) instead of on the main Ubuntu archive.
.SH AUTHORS .SH AUTHORS
\fBubuntu-build\fR was written by Martin Pitt <martin.pitt@canonical.com>, and \fBubuntu-build\fR was written by Martin Pitt <martin.pitt@canonical.com>, and
this manual page was written by Jonathan Patrick Davies <jpds@ubuntu.com>. this manual page was written by Jonathan Patrick Davies <jpds@ubuntu.com>.
.PP .PP
Both are released under the terms of the GNU General Public License, version 3 Both are released under the terms of the GNU General Public License, version 3.
or (at your option) any later version.

View File

@ -22,7 +22,10 @@
# UDT_EDIT_WRAPPER_TEMPLATE_RE: An extra boilerplate-detecting regex. # UDT_EDIT_WRAPPER_TEMPLATE_RE: An extra boilerplate-detecting regex.
# UDT_EDIT_WRAPPER_FILE_DESCRIPTION: The type of file being edited. # UDT_EDIT_WRAPPER_FILE_DESCRIPTION: The type of file being edited.
import optparse # pylint: disable=invalid-name
# pylint: enable=invalid-name
import argparse
import os import os
import re import re
@ -30,33 +33,30 @@ from ubuntutools.question import EditFile
def main(): def main():
parser = optparse.OptionParser('%prog [options] filename') parser = argparse.ArgumentParser(usage="%(prog)s [options] filename")
options, args = parser.parse_args() parser.add_argument("filename", help=argparse.SUPPRESS)
args = parser.parse_args()
if not os.path.isfile(args.filename):
parser.error(f"File {args.filename} does not exist")
if len(args) != 1: if "UDT_EDIT_WRAPPER_EDITOR" in os.environ:
parser.error('A filename must be specified') os.environ["EDITOR"] = os.environ["UDT_EDIT_WRAPPER_EDITOR"]
body = args[0]
if not os.path.isfile(body):
parser.error('File %s does not exist' % body)
if 'UDT_EDIT_WRAPPER_EDITOR' in os.environ:
os.environ['EDITOR'] = os.environ['UDT_EDIT_WRAPPER_EDITOR']
else: else:
del os.environ['EDITOR'] del os.environ["EDITOR"]
if 'UDT_EDIT_WRAPPER_VISUAL' in os.environ: if "UDT_EDIT_WRAPPER_VISUAL" in os.environ:
os.environ['VISUAL'] = os.environ['UDT_EDIT_WRAPPER_VISUAL'] os.environ["VISUAL"] = os.environ["UDT_EDIT_WRAPPER_VISUAL"]
else: else:
del os.environ['VISUAL'] del os.environ["VISUAL"]
placeholders = [] placeholders = []
if 'UDT_EDIT_WRAPPER_TEMPLATE_RE' in os.environ: if "UDT_EDIT_WRAPPER_TEMPLATE_RE" in os.environ:
placeholders.append(re.compile( placeholders.append(re.compile(os.environ["UDT_EDIT_WRAPPER_TEMPLATE_RE"]))
os.environ['UDT_EDIT_WRAPPER_TEMPLATE_RE']))
description = os.environ.get('UDT_EDIT_WRAPPER_FILE_DESCRIPTION', 'file') description = os.environ.get("UDT_EDIT_WRAPPER_FILE_DESCRIPTION", "file")
EditFile(body, description, placeholders).edit() EditFile(args.filename, description, placeholders).edit()
if __name__ == '__main__':
if __name__ == "__main__":
main() main()

View File

@ -19,63 +19,70 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import optparse # pylint: disable=invalid-name
import sys # pylint: enable=invalid-name
import argparse
import json import json
import sys
from httplib2 import Http, HttpLib2Error from httplib2 import Http, HttpLib2Error
import ubuntutools.misc import ubuntutools.misc
from ubuntutools import getLogger from ubuntutools import getLogger
Logger = getLogger() Logger = getLogger()
def main(): def main():
parser = optparse.OptionParser( parser = argparse.ArgumentParser(
usage='%prog [options] [string]', usage="%(prog)s [options] [string]",
description='List pending merges from Debian matching string') description="List pending merges from Debian matching string",
args = parser.parse_args()[1] )
parser.add_argument("string", nargs="?", help=argparse.SUPPRESS)
if len(args) > 1: args = parser.parse_args()
parser.error('Too many arguments')
elif len(args) == 1:
match = args[0]
else:
match = None
ubuntutools.misc.require_utf8() ubuntutools.misc.require_utf8()
for component in ('main', 'main-manual', for component in (
'restricted', 'restricted-manual', "main",
'universe', 'universe-manual', "main-manual",
'multiverse', 'multiverse-manual'): "restricted",
"restricted-manual",
url = 'https://merges.ubuntu.com/%s.json' % component "universe",
"universe-manual",
"multiverse",
"multiverse-manual",
):
url = f"https://merges.ubuntu.com/{component}.json"
try: try:
headers, page = Http().request(url) headers, page = Http().request(url)
except HttpLib2Error as e: except HttpLib2Error as e:
Logger.exception(e) Logger.exception(e)
sys.exit(1) sys.exit(1)
if headers.status != 200: if headers.status != 200:
Logger.error("%s: %s %s" % (url, headers.status, Logger.error("%s: %s %s", url, headers.status, headers.reason)
headers.reason))
sys.exit(1) sys.exit(1)
for merge in json.loads(page): for merge in json.loads(page):
package = merge['source_package'] package = merge["source_package"]
author, uploader = '', '' author, uploader = "", ""
if merge.get('user'): if merge.get("user"):
author = merge['user'] author = merge["user"]
if merge.get('uploader'): if merge.get("uploader"):
uploader = '(%s)' % merge['uploader'] uploader = f"({merge['uploader']})"
teams = merge.get('teams', []) teams = merge.get("teams", [])
pretty_uploader = '{} {}'.format(author, uploader) pretty_uploader = f"{author} {uploader}"
if (match is None or match in package or match in author if (
or match in uploader or match in teams): args.string is None
Logger.info('%s\t%s' % (package, pretty_uploader)) or args.string in package
or args.string in author
or args.string in uploader
or args.string in teams
):
Logger.info("%s\t%s", package, pretty_uploader)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -21,40 +21,213 @@
# #
# ################################################################## # ##################################################################
# pylint: disable=invalid-name
# pylint: enable=invalid-name
import argparse import argparse
import debianbts
import logging import logging
import re import re
import sys import sys
import webbrowser import webbrowser
from collections.abc import Iterable
from email.message import EmailMessage
import debianbts
from launchpadlib.launchpad import Launchpad from launchpadlib.launchpad import Launchpad
from ubuntutools import getLogger
from ubuntutools.config import UDTConfig from ubuntutools.config import UDTConfig
from ubuntutools import getLogger
Logger = getLogger() Logger = getLogger()
ATTACHMENT_MAX_SIZE = 2000
def main(): def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
"-b",
"--browserless",
action="store_true",
help="Don't open the bug in the browser at the end",
)
parser.add_argument(
"-l",
"--lpinstance",
metavar="INSTANCE",
help="LP instance to connect to (default: production)",
)
parser.add_argument(
"-v", "--verbose", action="store_true", help="Print info about the bug being imported"
)
parser.add_argument(
"-n",
"--dry-run",
action="store_true",
help="Don't actually open a bug (also sets verbose)",
)
parser.add_argument(
"-p", "--package", help="Launchpad package to file bug against (default: Same as Debian)"
)
parser.add_argument(
"--no-conf", action="store_true", help="Don't read config files or environment variables."
)
parser.add_argument("bugs", nargs="+", help="Bug number(s) or URL(s)")
return parser.parse_args()
def get_bug_numbers(bug_list: Iterable[str]) -> list[int]:
bug_re = re.compile(r"bug=(\d+)") bug_re = re.compile(r"bug=(\d+)")
parser = argparse.ArgumentParser() bug_nums = []
parser.add_argument("-b", "--browserless", action="store_true",
help="Don't open the bug in the browser at the end") for bug_num in bug_list:
parser.add_argument("-l", "--lpinstance", metavar="INSTANCE", if bug_num.startswith("http"):
help="LP instance to connect to (default: production)") # bug URL
parser.add_argument("-v", "--verbose", action="store_true", match = bug_re.search(bug_num)
help="Print info about the bug being imported") if match is None:
parser.add_argument("-n", "--dry-run", action="store_true", Logger.error("Can't determine bug number from %s", bug_num)
help="Don't actually open a bug (also sets verbose)") sys.exit(1)
parser.add_argument("-p", "--package", bug_num = match.groups()[0]
help="Launchpad package to file bug against " bug_num = bug_num.lstrip("#")
"(default: Same as Debian)") bug_nums.append(int(bug_num))
parser.add_argument("--no-conf", action="store_true",
help="Don't read config files or environment variables.") return bug_nums
parser.add_argument("bugs", nargs="+", help="Bug number(s) or URL(s)")
options = parser.parse_args()
def walk_multipart_message(message: EmailMessage) -> tuple[str, list[tuple[int, EmailMessage]]]:
summary = ""
attachments = []
i = 1
for part in message.walk():
content_type = part.get_content_type()
if content_type.startswith("multipart/"):
# we're already iterating on multipart items
# let's just skip the multipart extra metadata
continue
if content_type == "application/pgp-signature":
# we're not interested in importing pgp signatures
continue
if part.is_attachment():
attachments.append((i, part))
elif content_type.startswith("image/"):
# images here are not attachment, they are inline, but Launchpad can't handle that,
# so let's add them as attachments
summary += f"Message part #{i}\n"
summary += f"[inline image '{part.get_filename()}']\n\n"
attachments.append((i, part))
elif content_type.startswith("text/html"):
summary += f"Message part #{i}\n"
summary += "[inline html]\n\n"
attachments.append((i, part))
elif content_type == "text/plain":
summary += f"Message part #{i}\n"
summary += part.get_content() + "\n"
else:
raise RuntimeError(
f"""Unknown message part
Your Debian bug is too weird to be imported in Launchpad, sorry.
You can fix that by patching this script in ubuntu-dev-tools.
Faulty message part:
{part}"""
)
i += 1
return summary, attachments
def process_bugs(
bugs: Iterable[debianbts.Bugreport],
launchpad: Launchpad,
package: str,
dry_run: bool = True,
browserless: bool = False,
) -> bool:
debian = launchpad.distributions["debian"]
ubuntu = launchpad.distributions["ubuntu"]
lp_debbugs = launchpad.bug_trackers.getByName(name="debbugs")
err = False
for bug in bugs:
ubupackage = bug.source
if package:
ubupackage = package
bug_num = bug.bug_num
subject = bug.subject
log = debianbts.get_bug_log(bug_num)
message = log[0]["message"]
assert isinstance(message, EmailMessage)
attachments: list[tuple[int, EmailMessage]] = []
if message.is_multipart():
summary, attachments = walk_multipart_message(message)
else:
summary = str(message.get_payload())
target = ubuntu.getSourcePackage(name=ubupackage)
if target is None:
Logger.error(
"Source package '%s' is not in Ubuntu. Please specify "
"the destination source package with --package",
ubupackage,
)
err = True
continue
description = f"Imported from Debian bug http://bugs.debian.org/{bug_num}:\n\n{summary}"
# LP limits descriptions to 50K chars
description = (description[:49994] + " [...]") if len(description) > 50000 else description
Logger.debug("Target: %s", target)
Logger.debug("Subject: %s", subject)
Logger.debug("Description: ")
Logger.debug(description)
for i, attachment in attachments:
Logger.debug("Attachment #%s (%s)", i, attachment.get_filename() or "inline")
Logger.debug("Content:")
if attachment.get_content_type() == "text/plain":
content = attachment.get_content()
if len(content) > ATTACHMENT_MAX_SIZE:
content = (
content[:ATTACHMENT_MAX_SIZE]
+ f" [attachment cropped after {ATTACHMENT_MAX_SIZE} characters...]"
)
Logger.debug(content)
else:
Logger.debug("[data]")
if dry_run:
Logger.info("Dry-Run: not creating Ubuntu bug.")
continue
u_bug = launchpad.bugs.createBug(target=target, title=subject, description=description)
for i, attachment in attachments:
name = f"#{i}-{attachment.get_filename() or "inline"}"
content = attachment.get_content()
if isinstance(content, str):
# Launchpad only wants bytes
content = content.encode()
u_bug.addAttachment(
filename=name,
data=content,
comment=f"Imported from Debian bug http://bugs.debian.org/{bug_num}",
)
d_sp = debian.getSourcePackage(name=package)
if d_sp is None and package:
d_sp = debian.getSourcePackage(name=package)
d_task = u_bug.addTask(target=d_sp)
d_watch = u_bug.addWatch(remote_bug=bug_num, bug_tracker=lp_debbugs)
d_task.bug_watch = d_watch
d_task.lp_save()
Logger.info("Opened %s", u_bug.web_link)
if not browserless:
webbrowser.open(u_bug.web_link)
return err
def main() -> None:
options = parse_args()
config = UDTConfig(options.no_conf) config = UDTConfig(options.no_conf)
if options.lpinstance is None: if options.lpinstance is None:
@ -69,77 +242,15 @@ def main():
if options.verbose: if options.verbose:
Logger.setLevel(logging.DEBUG) Logger.setLevel(logging.DEBUG)
debian = launchpad.distributions['debian'] bugs = debianbts.get_status(get_bug_numbers(options.bugs))
ubuntu = launchpad.distributions['ubuntu']
lp_debbugs = launchpad.bug_trackers.getByName(name='debbugs')
bug_nums = []
for bug_num in options.bugs:
if bug_num.startswith("http"):
# bug URL
match = bug_re.search(bug_num)
if match is None:
Logger.error("Can't determine bug number from %s", bug_num)
sys.exit(1)
bug_num = match.groups()[0]
bug_num = bug_num.lstrip("#")
bug_num = int(bug_num)
bug_nums.append(bug_num)
bugs = debianbts.get_status(*bug_nums)
if not bugs: if not bugs:
Logger.error("Cannot find any of the listed bugs") Logger.error("Cannot find any of the listed bugs")
sys.exit(1) sys.exit(1)
err = False if process_bugs(bugs, launchpad, options.package, options.dry_run, options.browserless):
for bug in bugs:
ubupackage = package = bug.source
if options.package:
ubupackage = options.package
bug_num = bug.bug_num
subject = bug.subject
log = debianbts.get_bug_log(bug_num)
summary = log[0]['message'].get_payload()
target = ubuntu.getSourcePackage(name=ubupackage)
if target is None:
Logger.error("Source package '%s' is not in Ubuntu. Please specify "
"the destination source package with --package",
ubupackage)
err = True
continue
description = ('Imported from Debian bug http://bugs.debian.org/%d:\n\n%s' %
(bug_num, summary))
# LP limits descriptions to 50K chars
description = (description[:49994] + ' [...]') if len(description) > 50000 else description
Logger.debug('Target: %s' % target)
Logger.debug('Subject: %s' % subject)
Logger.debug('Description: ')
Logger.debug(description)
if options.dry_run:
Logger.info('Dry-Run: not creating Ubuntu bug.')
continue
u_bug = launchpad.bugs.createBug(target=target, title=subject,
description=description)
d_sp = debian.getSourcePackage(name=package)
if d_sp is None and options.package:
d_sp = debian.getSourcePackage(name=options.package)
d_task = u_bug.addTask(target=d_sp)
d_watch = u_bug.addWatch(remote_bug=bug_num, bug_tracker=lp_debbugs)
d_task.bug_watch = d_watch
d_task.lp_save()
Logger.info("Opened %s", u_bug.web_link)
if not options.browserless:
webbrowser.open(u_bug.web_link)
if err:
sys.exit(1) sys.exit(1)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -21,20 +21,20 @@
# Authors: # Authors:
# Daniel Holbach <daniel.holbach@canonical.com> # Daniel Holbach <daniel.holbach@canonical.com>
import argparse
import sys import sys
from optparse import OptionParser
from launchpadlib.launchpad import Launchpad
from launchpadlib.errors import HTTPError from launchpadlib.errors import HTTPError
from launchpadlib.launchpad import Launchpad
from ubuntutools.config import UDTConfig
from ubuntutools import getLogger from ubuntutools import getLogger
from ubuntutools.config import UDTConfig
Logger = getLogger() Logger = getLogger()
def error_out(msg): def error_out(msg, *args):
Logger.error(msg) Logger.error(msg, *args)
sys.exit(1) sys.exit(1)
@ -42,54 +42,64 @@ def save_entry(entry):
try: try:
entry.lp_save() entry.lp_save()
except HTTPError as error: except HTTPError as error:
error_out(error.content) error_out("%s", error.content)
def tag_bug(bug): def tag_bug(bug):
bug.tags = bug.tags + ['bitesize'] # LP: #254901 workaround bug.tags = bug.tags + ["bitesize"] # LP: #254901 workaround
save_entry(bug) save_entry(bug)
def main(): def main():
usage = "Usage: %prog <bug number>" parser = argparse.ArgumentParser(usage="%(prog)s [options] <bug number>")
opt_parser = OptionParser(usage) parser.add_argument(
opt_parser.add_option("-l", "--lpinstance", metavar="INSTANCE", "-l",
help="Launchpad instance to connect to " "--lpinstance",
"(default: production)", metavar="INSTANCE",
dest="lpinstance", default=None) help="Launchpad instance to connect to (default: production)",
opt_parser.add_option("--no-conf", dest="lpinstance",
help="Don't read config files or " default=None,
"environment variables.", )
dest="no_conf", default=False, action="store_true") parser.add_argument(
(options, args) = opt_parser.parse_args() "--no-conf",
config = UDTConfig(options.no_conf) help="Don't read config files or environment variables.",
if options.lpinstance is None: dest="no_conf",
options.lpinstance = config.get_value("LPINSTANCE") default=False,
if len(args) < 1: action="store_true",
opt_parser.error("Need at least one bug number.") )
parser.add_argument("bug_number", help=argparse.SUPPRESS)
args = parser.parse_args()
config = UDTConfig(args.no_conf)
if args.lpinstance is None:
args.lpinstance = config.get_value("LPINSTANCE")
launchpad = Launchpad.login_with("ubuntu-dev-tools", options.lpinstance) launchpad = Launchpad.login_with("ubuntu-dev-tools", args.lpinstance)
if launchpad is None: if launchpad is None:
error_out("Couldn't authenticate to Launchpad.") error_out("Couldn't authenticate to Launchpad.")
# check that the new main bug isn't a duplicate # check that the new main bug isn't a duplicate
try: try:
bug = launchpad.bugs[args[0]] bug = launchpad.bugs[args.bug_number]
except HTTPError as error: except HTTPError as error:
if error.response.status == 401: if error.response.status == 401:
error_out("Don't have enough permissions to access bug %s. %s" % error_out(
(args[0], error.content)) "Don't have enough permissions to access bug %s. %s",
args.bug_number,
error.content,
)
else: else:
raise raise
if 'bitesize' in bug.tags: if "bitesize" in bug.tags:
error_out("Bug is already marked as 'bitesize'.") error_out("Bug is already marked as 'bitesize'.")
bug.newMessage(content="I'm marking this bug as 'bitesize' as it looks " bug.newMessage(
"like an issue that is easy to fix and suitable " content="I'm marking this bug as 'bitesize' as it looks "
"for newcomers in Ubuntu development. If you need " "like an issue that is easy to fix and suitable "
"any help with fixing it, talk to me about it.") "for newcomers in Ubuntu development. If you need "
"any help with fixing it, talk to me about it."
)
bug.subscribe(person=launchpad.me) bug.subscribe(person=launchpad.me)
tag_bug(launchpad.bugs[bug.id]) # fresh bug object, LP: #336866 workaround tag_bug(launchpad.bugs[bug.id]) # fresh bug object, LP: #336866 workaround
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -18,24 +18,31 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=invalid-name
# pylint: enable=invalid-name
import sys import sys
from debian.changelog import Changelog from debian.changelog import Changelog
from ubuntutools import getLogger from ubuntutools import getLogger
Logger = getLogger() Logger = getLogger()
def usage(exit_code=1): def usage(exit_code=1):
Logger.info('''Usage: merge-changelog <left changelog> <right changelog> Logger.info(
"""Usage: merge-changelog <left changelog> <right changelog>
merge-changelog takes two changelogs that once shared a common source, merge-changelog takes two changelogs that once shared a common source,
merges them back together, and prints the merged result to stdout. This merges them back together, and prints the merged result to stdout. This
is useful if you need to manually merge a ubuntu package with a new is useful if you need to manually merge a ubuntu package with a new
Debian release of the package. Debian release of the package.
''') """
)
sys.exit(exit_code) sys.exit(exit_code)
######################################################################## ########################################################################
# Changelog Management # Changelog Management
######################################################################## ########################################################################
@ -44,9 +51,9 @@ Debian release of the package.
def merge_changelog(left_changelog, right_changelog): def merge_changelog(left_changelog, right_changelog):
"""Merge a changelog file.""" """Merge a changelog file."""
with open(left_changelog) as f: with open(left_changelog, encoding="utf-8") as f:
left_cl = Changelog(f) left_cl = Changelog(f)
with open(right_changelog) as f: with open(right_changelog, encoding="utf-8") as f:
right_cl = Changelog(f) right_cl = Changelog(f)
left_versions = set(left_cl.versions) left_versions = set(left_cl.versions)
@ -55,9 +62,9 @@ def merge_changelog(left_changelog, right_changelog):
right_blocks = iter(right_cl) right_blocks = iter(right_cl)
clist = sorted(left_versions | right_versions, reverse=True) clist = sorted(left_versions | right_versions, reverse=True)
ci = len(clist) remaining = len(clist)
for version in clist: for version in clist:
ci -= 1 remaining -= 1
if version in left_versions: if version in left_versions:
block = next(left_blocks) block = next(left_blocks)
if version in right_versions: if version in right_versions:
@ -67,11 +74,11 @@ def merge_changelog(left_changelog, right_changelog):
assert block.version == version assert block.version == version
Logger.info(str(block).strip() + ('\n' if ci else '')) Logger.info("%s%s", str(block).strip(), "\n" if remaining else "")
def main(): def main():
if len(sys.argv) > 1 and sys.argv[1] in ('-h', '--help'): if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
usage(0) usage(0)
if len(sys.argv) != 3: if len(sys.argv) != 3:
usage(1) usage(1)
@ -83,5 +90,5 @@ def main():
sys.exit(0) sys.exit(0)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

121
mk-sbuild
View File

@ -155,6 +155,7 @@ proxy="_unset_"
DEBOOTSTRAP_NO_CHECK_GPG=0 DEBOOTSTRAP_NO_CHECK_GPG=0
EATMYDATA=1 EATMYDATA=1
CCACHE=0 CCACHE=0
USE_PKGBINARYMANGLER=0
while :; do while :; do
case "$1" in case "$1" in
@ -166,7 +167,7 @@ while :; do
--arch) --arch)
CHROOT_ARCH="$2" CHROOT_ARCH="$2"
case $2 in case $2 in
armel|armhf|i386|lpia) armhf|i386)
if [ -z "$personality" ]; then if [ -z "$personality" ]; then
personality="linux32" personality="linux32"
fi fi
@ -303,10 +304,26 @@ if [ ! -w /var/lib/sbuild ]; then
# Prepare a usable default .sbuildrc # Prepare a usable default .sbuildrc
if [ ! -e ~/.sbuildrc ]; then if [ ! -e ~/.sbuildrc ]; then
cat > ~/.sbuildrc <<EOM cat > ~/.sbuildrc <<EOM
# *** VERIFY AND UPDATE \$mailto and \$maintainer_name BELOW *** # *** THIS COMMAND IS DEPRECATED ***
#
# In sbuild 0.87.0 and later, the unshare backend is available. This is
# expected to become the default in a future release.
#
# This is the new preferred way of building Debian packages, making the manual
# creation of schroots no longer necessary. To retain the default behavior,
# you may remove this comment block and continue.
#
# To test the unshare backend while retaining the default settings, run sbuild
# with --chroot-mode=unshare like this:
# $ sbuild --chroot-mode=unshare --dist=unstable hello
#
# To switch to the unshare backend by default (recommended), uncomment the
# following lines and delete the rest of the file (with the exception of the
# last two lines):
#\$chroot_mode = 'unshare';
#\$unshare_mmdebstrap_keep_tarball = 1;
# Mail address where logs are sent to (mandatory, no default!) # *** VERIFY AND UPDATE \$mailto and \$maintainer_name BELOW ***
\$mailto = '$USER';
# Name to use as override in .changes files for the Maintainer: field # Name to use as override in .changes files for the Maintainer: field
#\$maintainer_name='$USER <$USER@localhost>'; #\$maintainer_name='$USER <$USER@localhost>';
@ -397,29 +414,41 @@ fi
# By default DEBOOTSTRAP_SCRIPT must match RELEASE # By default DEBOOTSTRAP_SCRIPT must match RELEASE
DEBOOTSTRAP_SCRIPT="$RELEASE" DEBOOTSTRAP_SCRIPT="$RELEASE"
if [ "$DISTRO" = "ubuntu" ]; then dist_ge() {
ubuntu_dist_ge() { local releases="$($3-distro-info --all)"
local releases="$(ubuntu-distro-info --all)" local left=999
local left=999 local right=0
local right=0 local seq=1
local seq=1
for i in $releases; do for i in $releases; do
if [ $1 = $i ]; then if [ $1 = $i ]; then
local left=$seq local left=$seq
break break
fi fi
seq=$((seq+1)) seq=$((seq+1))
done done
seq=1
for i in $releases; do seq=1
for i in $releases; do
if [ $2 = $i ]; then if [ $2 = $i ]; then
local right=$seq local right=$seq
break break
fi fi
seq=$((seq+1)) seq=$((seq+1))
done done
[ $left -ge $right ] && return 0 || return 1
} [ $left -ge $right ] && return 0 || return 1
}
ubuntu_dist_ge () {
dist_ge $1 $2 ubuntu
}
debian_dist_ge () {
dist_ge $1 $2 debian
}
if [ "$DISTRO" = "ubuntu" ]; then
# On Ubuntu, set DEBOOTSTRAP_SCRIPT to gutsy to allow building new RELEASES without new debootstrap # On Ubuntu, set DEBOOTSTRAP_SCRIPT to gutsy to allow building new RELEASES without new debootstrap
DEBOOTSTRAP_SCRIPT=gutsy DEBOOTSTRAP_SCRIPT=gutsy
fi fi
@ -639,6 +668,7 @@ ubuntu)
if ubuntu_dist_ge "$RELEASE" "edgy"; then if ubuntu_dist_ge "$RELEASE" "edgy"; then
# Add pkgbinarymangler (edgy and later) # Add pkgbinarymangler (edgy and later)
BUILD_PKGS="$BUILD_PKGS pkgbinarymangler" BUILD_PKGS="$BUILD_PKGS pkgbinarymangler"
USE_PKGBINARYMANGLER=1
# Disable recommends for a smaller chroot (gutsy and later only) # Disable recommends for a smaller chroot (gutsy and later only)
if ubuntu_dist_ge "$RELEASE" "gutsy"; then if ubuntu_dist_ge "$RELEASE" "gutsy"; then
BUILD_PKGS="--no-install-recommends $BUILD_PKGS" BUILD_PKGS="--no-install-recommends $BUILD_PKGS"
@ -655,7 +685,7 @@ debian)
DEBOOTSTRAP_MIRROR="http://deb.debian.org/debian" DEBOOTSTRAP_MIRROR="http://deb.debian.org/debian"
fi fi
if [ -z "$COMPONENTS" ]; then if [ -z "$COMPONENTS" ]; then
COMPONENTS="main non-free contrib" COMPONENTS="main non-free non-free-firmware contrib"
fi fi
if [ -z "$SOURCES_PROPOSED_SUITE" ]; then if [ -z "$SOURCES_PROPOSED_SUITE" ]; then
SOURCES_PROPOSED_SUITE="RELEASE-proposed-updates" SOURCES_PROPOSED_SUITE="RELEASE-proposed-updates"
@ -663,7 +693,11 @@ debian)
# Debian only performs security updates # Debian only performs security updates
SKIP_UPDATES=1 SKIP_UPDATES=1
if [ -z "$SOURCES_SECURITY_SUITE" ]; then if [ -z "$SOURCES_SECURITY_SUITE" ]; then
SOURCES_SECURITY_SUITE="RELEASE/updates" if debian_dist_ge "$RELEASE" "bullseye"; then
SOURCES_SECURITY_SUITE="RELEASE-security"
else
SOURCES_SECURITY_SUITE="RELEASE/updates"
fi
fi fi
if [ -z "$SOURCES_SECURITY_URL" ]; then if [ -z "$SOURCES_SECURITY_URL" ]; then
SOURCES_SECURITY_URL="http://security.debian.org/" SOURCES_SECURITY_URL="http://security.debian.org/"
@ -734,12 +768,12 @@ DEBOOTSTRAP_COMMAND=debootstrap
if [ "$CHROOT_ARCH" != "$HOST_ARCH" ] ; then if [ "$CHROOT_ARCH" != "$HOST_ARCH" ] ; then
case "$CHROOT_ARCH-$HOST_ARCH" in case "$CHROOT_ARCH-$HOST_ARCH" in
# Sometimes we don't need qemu # Sometimes we don't need qemu
amd64-i386|amd64-lpia|armel-armhf|armhf-armel|arm64-armel|arm64-armhf|armel-arm64|armhf-arm64|i386-amd64|i386-lpia|lpia-i386|powerpc-ppc64|ppc64-powerpc|sparc-sparc64|sparc64-sparc) amd64-i386|arm64-armhf|armhf-arm64|i386-amd64|powerpc-ppc64|ppc64-powerpc)
;; ;;
# Sometimes we do # Sometimes we do
*) *)
DEBOOTSTRAP_COMMAND=qemu-debootstrap DEBOOTSTRAP_COMMAND=debootstrap
if ! which "$DEBOOTSTRAP_COMMAND"; then if ! which "qemu-x86_64-static"; then
sudo apt-get install qemu-user-static sudo apt-get install qemu-user-static
fi fi
;; ;;
@ -858,6 +892,13 @@ EOM
fi fi
fi fi
if [ -z "$SKIP_PROPOSED" ]; then if [ -z "$SKIP_PROPOSED" ]; then
TEMP_PREFERENCES=`mktemp -t preferences-XXXXXX`
cat >> "$TEMP_PREFERENCES" <<EOM
# override for NotAutomatic: yes
Package: *
Pin: release a=*-proposed
Pin-Priority: 500
EOM
cat >> "$TEMP_SOURCES" <<EOM cat >> "$TEMP_SOURCES" <<EOM
deb ${MIRROR_ARCHS}${DEBOOTSTRAP_MIRROR} $SOURCES_PROPOSED_SUITE ${COMPONENTS} deb ${MIRROR_ARCHS}${DEBOOTSTRAP_MIRROR} $SOURCES_PROPOSED_SUITE ${COMPONENTS}
deb-src ${DEBOOTSTRAP_MIRROR} $SOURCES_PROPOSED_SUITE ${COMPONENTS} deb-src ${DEBOOTSTRAP_MIRROR} $SOURCES_PROPOSED_SUITE ${COMPONENTS}
@ -883,9 +924,12 @@ fi
cat "$TEMP_SOURCES" | sed -e "s|RELEASE|$RELEASE|g" | \ cat "$TEMP_SOURCES" | sed -e "s|RELEASE|$RELEASE|g" | \
sudo bash -c "cat > $MNT/etc/apt/sources.list" sudo bash -c "cat > $MNT/etc/apt/sources.list"
rm -f "$TEMP_SOURCES" rm -f "$TEMP_SOURCES"
if [ -n "$TEMP_PREFERENCES" ]; then
sudo mv "$TEMP_PREFERENCES" $MNT/etc/apt/preferences.d/proposed.pref
fi
# Copy the timezone (comment this out if you want to leave the chroot at UTC) # Copy the timezone (uncomment this if you want to use your local time zone)
sudo cp -P --remove-destination /etc/localtime /etc/timezone "$MNT"/etc/ #sudo cp -P --remove-destination /etc/localtime /etc/timezone "$MNT"/etc/
# Create a schroot entry for this chroot # Create a schroot entry for this chroot
TEMP_SCHROOTCONF=`mktemp -t schrootconf-XXXXXX` TEMP_SCHROOTCONF=`mktemp -t schrootconf-XXXXXX`
TEMPLATE_SCHROOTCONF=~/.mk-sbuild.schroot.conf TEMPLATE_SCHROOTCONF=~/.mk-sbuild.schroot.conf
@ -1004,6 +1048,25 @@ EOF
EOM EOM
fi fi
if [ "$USE_PKGBINARYMANGLER" = 1 ]; then
sudo bash -c "cat >> $MNT/finish.sh" <<EOM
mkdir -p /etc/pkgbinarymangler/
cat > /etc/pkgbinarymangler/maintainermangler.conf <<EOF
# pkgmaintainermangler configuration file
# pkgmaintainermangler will do nothing unless enable is set to "true"
enable: true
# Configure what happens if /CurrentlyBuilding is present, but invalid
# (i. e. it does not contain a Package: field). If "ignore" (default),
# the file is ignored (i. e. the Maintainer field is mangled) and a
# warning is printed. If "fail" (or any other value), pkgmaintainermangler
# exits with an error, which causes a package build to fail.
invalid_currentlybuilding: ignore
EOF
EOM
fi
if [ -n "$TARGET_ARCH" ]; then if [ -n "$TARGET_ARCH" ]; then
sudo bash -c "cat >> $MNT/finish.sh" <<EOM sudo bash -c "cat >> $MNT/finish.sh" <<EOM
# Configure target architecture # Configure target architecture
@ -1022,7 +1085,7 @@ apt-get update || true
echo set debconf/frontend Noninteractive | debconf-communicate echo set debconf/frontend Noninteractive | debconf-communicate
echo set debconf/priority critical | debconf-communicate echo set debconf/priority critical | debconf-communicate
# Install basic build tool set, trying to match buildd # Install basic build tool set, trying to match buildd
apt-get -y --force-yes install $BUILD_PKGS apt-get -y --force-yes -o Dpkg::Options::="--force-confold" install $BUILD_PKGS
# Set up expected /dev entries # Set up expected /dev entries
if [ ! -r /dev/stdin ]; then ln -s /proc/self/fd/0 /dev/stdin; fi if [ ! -r /dev/stdin ]; then ln -s /proc/self/fd/0 /dev/stdin; fi
if [ ! -r /dev/stdout ]; then ln -s /proc/self/fd/1 /dev/stdout; fi if [ ! -r /dev/stdout ]; then ln -s /proc/self/fd/1 /dev/stdout; fi

View File

@ -29,26 +29,29 @@
# configurations. For example, a symlink called pbuilder-hardy will assume # configurations. For example, a symlink called pbuilder-hardy will assume
# that the target distribution is always meant to be Ubuntu Hardy. # that the target distribution is always meant to be Ubuntu Hardy.
# pylint: disable=invalid-name
# pylint: enable=invalid-name
import os import os
import os.path import os.path
import sys
import subprocess
import shutil import shutil
import subprocess
import sys
from contextlib import suppress
import debian.deb822 import debian.deb822
from contextlib import suppress from distro_info import DebianDistroInfo, DistroDataOutdated, UbuntuDistroInfo
from distro_info import DebianDistroInfo, UbuntuDistroInfo, DistroDataOutdated
import ubuntutools.misc import ubuntutools.misc
import ubuntutools.version import ubuntutools.version
from ubuntutools import getLogger
from ubuntutools.config import UDTConfig from ubuntutools.config import UDTConfig
from ubuntutools.question import YesNoQuestion from ubuntutools.question import YesNoQuestion
from ubuntutools import getLogger
Logger = getLogger() Logger = getLogger()
class PbuilderDist(object): class PbuilderDist:
def __init__(self, builder): def __init__(self, builder):
# Base directory where pbuilder will put all the files it creates. # Base directory where pbuilder will put all the files it creates.
self.base = None self.base = None
@ -87,32 +90,36 @@ class PbuilderDist(object):
self.chroot_string = None self.chroot_string = None
# Authentication method # Authentication method
self.auth = 'sudo' self.auth = "sudo"
# Builder # Builder
self.builder = builder self.builder = builder
self._debian_distros = DebianDistroInfo().all + \ # Distro info
['stable', 'testing', 'unstable'] self.debian_distro_info = DebianDistroInfo()
self.ubuntu_distro_info = UbuntuDistroInfo()
self._debian_distros = self.debian_distro_info.all + ["stable", "testing", "unstable"]
# Ensure that the used builder is installed # Ensure that the used builder is installed
paths = set(os.environ['PATH'].split(':')) paths = set(os.environ["PATH"].split(":"))
paths |= set(('/sbin', '/usr/sbin', '/usr/local/sbin')) paths |= set(("/sbin", "/usr/sbin", "/usr/local/sbin"))
if not any(os.path.exists(os.path.join(p, builder)) for p in paths): if not any(os.path.exists(os.path.join(p, builder)) for p in paths):
Logger.error('Could not find "%s".', builder) Logger.error('Could not find "%s".', builder)
sys.exit(1) sys.exit(1)
############################################################## ##############################################################
self.base = os.path.expanduser(os.environ.get('PBUILDFOLDER', self.base = os.path.expanduser(os.environ.get("PBUILDFOLDER", "~/pbuilder/"))
'~/pbuilder/'))
if 'SUDO_USER' in os.environ: if "SUDO_USER" in os.environ:
Logger.warning('Running under sudo. ' Logger.warning(
'This is probably not what you want. ' "Running under sudo. "
'pbuilder-dist will use sudo itself, ' "This is probably not what you want. "
'when necessary.') "pbuilder-dist will use sudo itself, "
if os.stat(os.environ['HOME']).st_uid != os.getuid(): "when necessary."
)
if os.stat(os.environ["HOME"]).st_uid != os.getuid():
Logger.error("You don't own $HOME") Logger.error("You don't own $HOME")
sys.exit(1) sys.exit(1)
@ -123,8 +130,8 @@ class PbuilderDist(object):
Logger.error('Cannot create base directory "%s"', self.base) Logger.error('Cannot create base directory "%s"', self.base)
sys.exit(1) sys.exit(1)
if 'PBUILDAUTH' in os.environ: if "PBUILDAUTH" in os.environ:
self.auth = os.environ['PBUILDAUTH'] self.auth = os.environ["PBUILDAUTH"]
self.system_architecture = ubuntutools.misc.host_architecture() self.system_architecture = ubuntutools.misc.host_architecture()
self.system_distro = ubuntutools.misc.system_distribution() self.system_distro = ubuntutools.misc.system_distribution()
@ -134,7 +141,7 @@ class PbuilderDist(object):
self.target_distro = self.system_distro self.target_distro = self.system_distro
def set_target_distro(self, distro): def set_target_distro(self, distro):
""" PbuilderDist.set_target_distro(distro) -> None """PbuilderDist.set_target_distro(distro) -> None
Check if the given target distribution name is correct, if it Check if the given target distribution name is correct, if it
isn't know to the system ask the user for confirmation before isn't know to the system ask the user for confirmation before
@ -145,16 +152,17 @@ class PbuilderDist(object):
Logger.error('"%s" is an invalid distribution codename.', distro) Logger.error('"%s" is an invalid distribution codename.', distro)
sys.exit(1) sys.exit(1)
if not os.path.isfile(os.path.join('/usr/share/debootstrap/scripts/', if not os.path.isfile(os.path.join("/usr/share/debootstrap/scripts/", distro)):
distro)): if os.path.isdir("/usr/share/debootstrap/scripts/"):
if os.path.isdir('/usr/share/debootstrap/scripts/'):
# Debian experimental doesn't have a debootstrap file but # Debian experimental doesn't have a debootstrap file but
# should work nevertheless. # should work nevertheless. Ubuntu releases automatically use
if distro not in self._debian_distros: # the gutsy script as of debootstrap 1.0.128+nmu2ubuntu1.1.
question = ('Warning: Unknown distribution "%s". ' if distro not in (self._debian_distros + self.ubuntu_distro_info.all):
'Do you want to continue' % distro) question = (
answer = YesNoQuestion().ask(question, 'no') f'Warning: Unknown distribution "{distro}". ' "Do you want to continue"
if answer == 'no': )
answer = YesNoQuestion().ask(question, "no")
if answer == "no":
sys.exit(0) sys.exit(0)
else: else:
Logger.error('Please install package "debootstrap".') Logger.error('Please install package "debootstrap".')
@ -163,33 +171,34 @@ class PbuilderDist(object):
self.target_distro = distro self.target_distro = distro
def set_operation(self, operation): def set_operation(self, operation):
""" PbuilderDist.set_operation -> None """PbuilderDist.set_operation -> None
Check if the given string is a valid pbuilder operation and Check if the given string is a valid pbuilder operation and
depending on this either save it into the appropiate variable depending on this either save it into the appropiate variable
or finalize pbuilder-dist's execution. or finalize pbuilder-dist's execution.
""" """
arguments = ('create', 'update', 'build', 'clean', 'login', 'execute') arguments = ("create", "update", "build", "clean", "login", "execute")
if operation not in arguments: if operation not in arguments:
if operation.endswith('.dsc'): if operation.endswith(".dsc"):
if os.path.isfile(operation): if os.path.isfile(operation):
self.operation = 'build' self.operation = "build"
return [operation] return [operation]
else: Logger.error('Could not find file "%s".', operation)
Logger.error('Could not find file "%s".', operation)
sys.exit(1)
else:
Logger.error('"%s" is not a recognized argument.\n'
'Please use one of these: %s.',
operation, ', '.join(arguments))
sys.exit(1) sys.exit(1)
else:
self.operation = operation Logger.error(
return [] '"%s" is not a recognized argument.\nPlease use one of these: %s.',
operation,
", ".join(arguments),
)
sys.exit(1)
self.operation = operation
return []
def get_command(self, remaining_arguments=None): def get_command(self, remaining_arguments=None):
""" PbuilderDist.get_command -> string """PbuilderDist.get_command -> string
Generate the pbuilder command which matches the given configuration Generate the pbuilder command which matches the given configuration
and return it as a string. and return it as a string.
@ -200,30 +209,34 @@ class PbuilderDist(object):
if self.build_architecture == self.system_architecture: if self.build_architecture == self.system_architecture:
self.chroot_string = self.target_distro self.chroot_string = self.target_distro
else: else:
self.chroot_string = (self.target_distro + '-' self.chroot_string = self.target_distro + "-" + self.build_architecture
+ self.build_architecture)
prefix = os.path.join(self.base, self.chroot_string) prefix = os.path.join(self.base, self.chroot_string)
if '--buildresult' not in remaining_arguments: if "--buildresult" not in remaining_arguments:
result = os.path.normpath('%s_result/' % prefix) result = os.path.normpath(f"{prefix}_result/")
else: else:
location_of_arg = remaining_arguments.index('--buildresult') location_of_arg = remaining_arguments.index("--buildresult")
result = os.path.normpath(remaining_arguments[location_of_arg+1]) result = os.path.normpath(remaining_arguments[location_of_arg + 1])
remaining_arguments.pop(location_of_arg+1) remaining_arguments.pop(location_of_arg + 1)
remaining_arguments.pop(location_of_arg) remaining_arguments.pop(location_of_arg)
if not self.logfile and self.operation != 'login': if not self.logfile and self.operation != "login":
if self.operation == 'build': if self.operation == "build":
dsc_files = [a for a in remaining_arguments dsc_files = [a for a in remaining_arguments if a.strip().endswith(".dsc")]
if a.strip().endswith('.dsc')]
assert len(dsc_files) == 1 assert len(dsc_files) == 1
dsc = debian.deb822.Dsc(open(dsc_files[0])) dsc = debian.deb822.Dsc(open(dsc_files[0], encoding="utf-8"))
version = ubuntutools.version.Version(dsc['Version']) version = ubuntutools.version.Version(dsc["Version"])
name = (dsc['Source'] + '_' + version.strip_epoch() + '_' + name = (
self.build_architecture + '.build') dsc["Source"]
+ "_"
+ version.strip_epoch()
+ "_"
+ self.build_architecture
+ ".build"
)
self.logfile = os.path.join(result, name) self.logfile = os.path.join(result, name)
else: else:
self.logfile = os.path.join(result, 'last_operation.log') self.logfile = os.path.join(result, "last_operation.log")
if not os.path.isdir(result): if not os.path.isdir(result):
try: try:
@ -233,90 +246,89 @@ class PbuilderDist(object):
sys.exit(1) sys.exit(1)
arguments = [ arguments = [
'--%s' % self.operation, f"--{self.operation}",
'--distribution', self.target_distro, "--distribution",
'--buildresult', result, self.target_distro,
"--buildresult",
result,
] ]
if self.operation == 'update': if self.operation == "update":
arguments += ['--override-config'] arguments += ["--override-config"]
if self.builder == 'pbuilder': if self.builder == "pbuilder":
arguments += ['--basetgz', prefix + '-base.tgz'] arguments += ["--basetgz", prefix + "-base.tgz"]
elif self.builder == 'cowbuilder': elif self.builder == "cowbuilder":
arguments += ['--basepath', prefix + '-base.cow'] arguments += ["--basepath", prefix + "-base.cow"]
else: else:
Logger.error('Unrecognized builder "%s".', self.builder) Logger.error('Unrecognized builder "%s".', self.builder)
sys.exit(1) sys.exit(1)
if self.logfile: if self.logfile:
arguments += ['--logfile', self.logfile] arguments += ["--logfile", self.logfile]
if os.path.exists('/var/cache/archive/'): if os.path.exists("/var/cache/archive/"):
arguments += ['--bindmounts', '/var/cache/archive/'] arguments += ["--bindmounts", "/var/cache/archive/"]
config = UDTConfig() config = UDTConfig()
if self.target_distro in self._debian_distros: if self.target_distro in self._debian_distros:
mirror = os.environ.get('MIRRORSITE', mirror = os.environ.get("MIRRORSITE", config.get_value("DEBIAN_MIRROR"))
config.get_value('DEBIAN_MIRROR')) components = "main"
components = 'main'
if self.extra_components: if self.extra_components:
components += ' contrib non-free' components += " contrib non-free non-free-firmware"
else: else:
mirror = os.environ.get('MIRRORSITE', mirror = os.environ.get("MIRRORSITE", config.get_value("UBUNTU_MIRROR"))
config.get_value('UBUNTU_MIRROR')) if self.build_architecture not in ("amd64", "i386"):
if self.build_architecture not in ('amd64', 'i386'): mirror = os.environ.get("MIRRORSITE", config.get_value("UBUNTU_PORTS_MIRROR"))
mirror = os.environ.get( components = "main restricted"
'MIRRORSITE', config.get_value('UBUNTU_PORTS_MIRROR'))
components = 'main restricted'
if self.extra_components: if self.extra_components:
components += ' universe multiverse' components += " universe multiverse"
arguments += ['--mirror', mirror] arguments += ["--mirror", mirror]
othermirrors = [] othermirrors = []
localrepo = '/var/cache/archive/' + self.target_distro localrepo = f"/var/cache/archive/{self.target_distro}"
if os.path.exists(localrepo): if os.path.exists(localrepo):
repo = 'deb file:///var/cache/archive/ %s/' % self.target_distro repo = f"deb file:///var/cache/archive/ {self.target_distro}/"
othermirrors.append(repo) othermirrors.append(repo)
if self.target_distro in self._debian_distros: if self.target_distro in self._debian_distros:
debian_info = DebianDistroInfo()
try: try:
codename = debian_info.codename(self.target_distro, codename = self.debian_distro_info.codename(
default=self.target_distro) self.target_distro, default=self.target_distro
)
except DistroDataOutdated as error: except DistroDataOutdated as error:
Logger.warning(error) Logger.warning(error)
if codename in (debian_info.devel(), 'experimental'): if codename in (self.debian_distro_info.devel(), "experimental"):
self.enable_security = False self.enable_security = False
self.enable_updates = False self.enable_updates = False
self.enable_proposed = False self.enable_proposed = False
elif codename in (debian_info.testing(), 'testing'): elif codename in (self.debian_distro_info.testing(), "testing"):
self.enable_updates = False self.enable_updates = False
if self.enable_security: if self.enable_security:
pocket = '-security' pocket = "-security"
with suppress(ValueError): with suppress(ValueError):
# before bullseye (version 11) security suite is /updates # before bullseye (version 11) security suite is /updates
if float(debian_info.version(codename)) < 11.0: if float(self.debian_distro_info.version(codename)) < 11.0:
pocket = '/updates' pocket = "/updates"
othermirrors.append('deb %s %s%s %s' othermirrors.append(
% (config.get_value('DEBSEC_MIRROR'), f"deb {config.get_value('DEBSEC_MIRROR')}"
self.target_distro, pocket, components)) f" {self.target_distro}{pocket} {components}"
)
if self.enable_updates: if self.enable_updates:
othermirrors.append('deb %s %s-updates %s' othermirrors.append(f"deb {mirror} {self.target_distro}-updates {components}")
% (mirror, self.target_distro, components))
if self.enable_proposed: if self.enable_proposed:
othermirrors.append('deb %s %s-proposed-updates %s' othermirrors.append(
% (mirror, self.target_distro, components)) f"deb {mirror} {self.target_distro}-proposed-updates {components}"
)
if self.enable_backports: if self.enable_backports:
othermirrors.append('deb %s %s-backports %s' othermirrors.append(f"deb {mirror} {self.target_distro}-backports {components}")
% (mirror, self.target_distro, components))
aptcache = os.path.join(self.base, 'aptcache', 'debian') aptcache = os.path.join(self.base, "aptcache", "debian")
else: else:
try: try:
dev_release = self.target_distro == UbuntuDistroInfo().devel() dev_release = self.target_distro == self.ubuntu_distro_info.devel()
except DistroDataOutdated as error: except DistroDataOutdated as error:
Logger.warning(error) Logger.warning(error)
dev_release = True dev_release = True
@ -326,46 +338,45 @@ class PbuilderDist(object):
self.enable_updates = False self.enable_updates = False
if self.enable_security: if self.enable_security:
othermirrors.append('deb %s %s-security %s' othermirrors.append(f"deb {mirror} {self.target_distro}-security {components}")
% (mirror, self.target_distro, components))
if self.enable_updates: if self.enable_updates:
othermirrors.append('deb %s %s-updates %s' othermirrors.append(f"deb {mirror} {self.target_distro}-updates {components}")
% (mirror, self.target_distro, components))
if self.enable_proposed: if self.enable_proposed:
othermirrors.append('deb %s %s-proposed %s' othermirrors.append(f"deb {mirror} {self.target_distro}-proposed {components}")
% (mirror, self.target_distro, components))
aptcache = os.path.join(self.base, 'aptcache', 'ubuntu') aptcache = os.path.join(self.base, "aptcache", "ubuntu")
if 'OTHERMIRROR' in os.environ: if "OTHERMIRROR" in os.environ:
othermirrors += os.environ['OTHERMIRROR'].split('|') othermirrors += os.environ["OTHERMIRROR"].split("|")
if othermirrors: if othermirrors:
arguments += ['--othermirror', '|'.join(othermirrors)] arguments += ["--othermirror", "|".join(othermirrors)]
# Work around LP:#599695 # Work around LP:#599695
if (ubuntutools.misc.system_distribution() == 'Debian' if (
and self.target_distro not in self._debian_distros): ubuntutools.misc.system_distribution() == "Debian"
if not os.path.exists( and self.target_distro not in self._debian_distros
'/usr/share/keyrings/ubuntu-archive-keyring.gpg'): ):
Logger.error('ubuntu-keyring not installed') if not os.path.exists("/usr/share/keyrings/ubuntu-archive-keyring.gpg"):
Logger.error("ubuntu-keyring not installed")
sys.exit(1) sys.exit(1)
arguments += [ arguments += [
'--debootstrapopts', "--debootstrapopts",
'--keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg', "--keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg",
] ]
elif (ubuntutools.misc.system_distribution() == 'Ubuntu' elif (
and self.target_distro in self._debian_distros): ubuntutools.misc.system_distribution() == "Ubuntu"
if not os.path.exists( and self.target_distro in self._debian_distros
'/usr/share/keyrings/debian-archive-keyring.gpg'): ):
Logger.error('debian-archive-keyring not installed') if not os.path.exists("/usr/share/keyrings/debian-archive-keyring.gpg"):
Logger.error("debian-archive-keyring not installed")
sys.exit(1) sys.exit(1)
arguments += [ arguments += [
'--debootstrapopts', "--debootstrapopts",
'--keyring=/usr/share/keyrings/debian-archive-keyring.gpg', "--keyring=/usr/share/keyrings/debian-archive-keyring.gpg",
] ]
arguments += ['--aptcache', aptcache, '--components', components] arguments += ["--aptcache", aptcache, "--components", components]
if not os.path.isdir(aptcache): if not os.path.isdir(aptcache):
try: try:
@ -375,13 +386,11 @@ class PbuilderDist(object):
sys.exit(1) sys.exit(1)
if self.build_architecture != self.system_architecture: if self.build_architecture != self.system_architecture:
arguments += ['--debootstrapopts', arguments += ["--debootstrapopts", "--arch=" + self.build_architecture]
'--arch=' + self.build_architecture]
apt_conf_dir = os.path.join(self.base, apt_conf_dir = os.path.join(self.base, f"etc/{self.target_distro}/apt.conf")
'etc/%s/apt.conf' % self.target_distro)
if os.path.exists(apt_conf_dir): if os.path.exists(apt_conf_dir):
arguments += ['--aptconfdir', apt_conf_dir] arguments += ["--aptconfdir", apt_conf_dir]
# Append remaining arguments # Append remaining arguments
if remaining_arguments: if remaining_arguments:
@ -392,28 +401,28 @@ class PbuilderDist(object):
# With both common variable name schemes (BTS: #659060). # With both common variable name schemes (BTS: #659060).
return [ return [
self.auth, self.auth,
'HOME=' + os.path.expanduser('~'), "HOME=" + os.path.expanduser("~"),
'ARCHITECTURE=' + self.build_architecture, "ARCHITECTURE=" + self.build_architecture,
'DISTRIBUTION=' + self.target_distro, "DISTRIBUTION=" + self.target_distro,
'ARCH=' + self.build_architecture, "ARCH=" + self.build_architecture,
'DIST=' + self.target_distro, "DIST=" + self.target_distro,
'DEB_BUILD_OPTIONS=' + os.environ.get('DEB_BUILD_OPTIONS', ''), "DEB_BUILD_OPTIONS=" + os.environ.get("DEB_BUILD_OPTIONS", ""),
self.builder, self.builder,
] + arguments ] + arguments
def show_help(exit_code=0): def show_help(exit_code=0):
""" help() -> None """help() -> None
Print a help message for pbuilder-dist, and exit with the given code. Print a help message for pbuilder-dist, and exit with the given code.
""" """
Logger.info('See man pbuilder-dist for more information.') Logger.info("See man pbuilder-dist for more information.")
sys.exit(exit_code) sys.exit(exit_code)
def main(): def main():
""" main() -> None """main() -> None
This is pbuilder-dist's main function. It creates a PbuilderDist This is pbuilder-dist's main function. It creates a PbuilderDist
object, modifies all necessary settings taking data from the object, modifies all necessary settings taking data from the
@ -421,27 +430,25 @@ def main():
the script and runs pbuilder itself or exists with an error message. the script and runs pbuilder itself or exists with an error message.
""" """
script_name = os.path.basename(sys.argv[0]) script_name = os.path.basename(sys.argv[0])
parts = script_name.split('-') parts = script_name.split("-")
# Copy arguments into another list for save manipulation # Copy arguments into another list for save manipulation
args = sys.argv[1:] args = sys.argv[1:]
if ('-' in script_name and parts[0] not in ('pbuilder', 'cowbuilder') if "-" in script_name and parts[0] not in ("pbuilder", "cowbuilder") or len(parts) > 3:
or len(parts) > 3): Logger.error('"%s" is not a valid name for a "pbuilder-dist" executable.', script_name)
Logger.error('"%s" is not a valid name for a "pbuilder-dist" '
'executable.', script_name)
sys.exit(1) sys.exit(1)
if len(args) < 1: if len(args) < 1:
Logger.error('Insufficient number of arguments.') Logger.error("Insufficient number of arguments.")
show_help(1) show_help(1)
if args[0] in ('-h', '--help', 'help'): if args[0] in ("-h", "--help", "help"):
show_help(0) show_help(0)
app = PbuilderDist(parts[0]) app = PbuilderDist(parts[0])
if len(parts) > 1 and parts[1] != 'dist' and '.' not in parts[1]: if len(parts) > 1 and parts[1] != "dist" and "." not in parts[1]:
app.set_target_distro(parts[1]) app.set_target_distro(parts[1])
else: else:
app.set_target_distro(args.pop(0)) app.set_target_distro(args.pop(0))
@ -449,24 +456,31 @@ def main():
if len(parts) > 2: if len(parts) > 2:
requested_arch = parts[2] requested_arch = parts[2]
elif len(args) > 0: elif len(args) > 0:
if shutil.which('arch-test') is not None: if shutil.which("arch-test") is not None:
if subprocess.run( arch_test = subprocess.run(
['arch-test', args[0]], ["arch-test", args[0]], check=False, stdout=subprocess.DEVNULL
stdout=subprocess.DEVNULL).returncode == 0: )
if arch_test.returncode == 0:
requested_arch = args.pop(0) requested_arch = args.pop(0)
elif (os.path.isdir('/usr/lib/arch-test') elif os.path.isdir("/usr/lib/arch-test") and args[0] in os.listdir(
and args[0] in os.listdir('/usr/lib/arch-test/')): "/usr/lib/arch-test/"
Logger.error('Architecture "%s" is not supported on your ' ):
'currently running kernel. Consider installing ' Logger.error(
'the qemu-user-static package to enable the use of ' 'Architecture "%s" is not supported on your '
'foreign architectures.', args[0]) "currently running kernel. Consider installing "
"the qemu-user-static package to enable the use of "
"foreign architectures.",
args[0],
)
sys.exit(1) sys.exit(1)
else: else:
requested_arch = None requested_arch = None
else: else:
Logger.error('Cannot determine if "%s" is a valid architecture. ' Logger.error(
'Please install the arch-test package and retry.', 'Cannot determine if "%s" is a valid architecture. '
args[0]) "Please install the arch-test package and retry.",
args[0],
)
sys.exit(1) sys.exit(1)
else: else:
requested_arch = None requested_arch = None
@ -474,62 +488,64 @@ def main():
if requested_arch: if requested_arch:
app.build_architecture = requested_arch app.build_architecture = requested_arch
# For some foreign architectures we need to use qemu # For some foreign architectures we need to use qemu
if (requested_arch != app.system_architecture if requested_arch != app.system_architecture and (
and (app.system_architecture, requested_arch) not in [ app.system_architecture,
('amd64', 'i386'), ('amd64', 'lpia'), ('arm', 'armel'), requested_arch,
('armel', 'arm'), ('armel', 'armhf'), ('armhf', 'armel'), ) not in [
('arm64', 'arm'), ('arm64', 'armhf'), ('arm64', 'armel'), ("amd64", "i386"),
('i386', 'lpia'), ('lpia', 'i386'), ('powerpc', 'ppc64'), ("arm64", "arm"),
('ppc64', 'powerpc'), ('sparc', 'sparc64'), ("arm64", "armhf"),
('sparc64', 'sparc')]): ("powerpc", "ppc64"),
args += ['--debootstrap', 'qemu-debootstrap'] ("ppc64", "powerpc"),
]:
args += ["--debootstrap", "debootstrap"]
if 'mainonly' in sys.argv or '--main-only' in sys.argv: if "mainonly" in sys.argv or "--main-only" in sys.argv:
app.extra_components = False app.extra_components = False
if 'mainonly' in sys.argv: if "mainonly" in sys.argv:
args.remove('mainonly') args.remove("mainonly")
else: else:
args.remove('--main-only') args.remove("--main-only")
if '--release-only' in sys.argv: if "--release-only" in sys.argv:
args.remove('--release-only') args.remove("--release-only")
app.enable_security = False app.enable_security = False
app.enable_updates = False app.enable_updates = False
app.enable_proposed = False app.enable_proposed = False
elif '--security-only' in sys.argv: elif "--security-only" in sys.argv:
args.remove('--security-only') args.remove("--security-only")
app.enable_updates = False app.enable_updates = False
app.enable_proposed = False app.enable_proposed = False
elif '--updates-only' in sys.argv: elif "--updates-only" in sys.argv:
args.remove('--updates-only') args.remove("--updates-only")
app.enable_proposed = False app.enable_proposed = False
elif '--backports' in sys.argv: elif "--backports" in sys.argv:
args.remove('--backports') args.remove("--backports")
app.enable_backports = True app.enable_backports = True
if len(args) < 1: if len(args) < 1:
Logger.error('Insufficient number of arguments.') Logger.error("Insufficient number of arguments.")
show_help(1) show_help(1)
# Parse the operation # Parse the operation
args = app.set_operation(args.pop(0)) + args args = app.set_operation(args.pop(0)) + args
if app.operation == 'build': if app.operation == "build":
if len([a for a in args if a.strip().endswith('.dsc')]) != 1: if len([a for a in args if a.strip().endswith(".dsc")]) != 1:
msg = 'You have to specify one .dsc file if you want to build.' msg = "You have to specify one .dsc file if you want to build."
Logger.error(msg) Logger.error(msg)
sys.exit(1) sys.exit(1)
# Execute the pbuilder command # Execute the pbuilder command
if '--debug-echo' not in args: if "--debug-echo" not in args:
sys.exit(subprocess.call(app.get_command(args))) sys.exit(subprocess.call(app.get_command(args)))
else: else:
Logger.info(app.get_command([arg for arg in args if arg != '--debug-echo'])) Logger.info(app.get_command([arg for arg in args if arg != "--debug-echo"]))
if __name__ == '__main__': if __name__ == "__main__":
try: try:
main() main()
except KeyboardInterrupt: except KeyboardInterrupt:
Logger.error('Manually aborted.') Logger.error("Manually aborted.")
sys.exit(1) sys.exit(1)

142
pm-helper Executable file
View File

@ -0,0 +1,142 @@
#!/usr/bin/python3
# Find the next thing to work on for proposed-migration
# Copyright (C) 2023 Canonical Ltd.
# Author: Steve Langasek <steve.langasek@ubuntu.com>
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License, version 3.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import lzma
import sys
import webbrowser
from argparse import ArgumentParser
import yaml
from launchpadlib.launchpad import Launchpad
from ubuntutools.utils import get_url
# proposed-migration is only concerned with the devel series; unlike other
# tools, don't make this configurable
excuses_url = "https://ubuntu-archive-team.ubuntu.com/proposed-migration/update_excuses.yaml.xz"
def get_proposed_version(excuses, package):
for k in excuses["sources"]:
if k["source"] == package:
return k.get("new-version")
return None
def claim_excuses_bug(launchpad, bug, package):
print(f"LP: #{bug.id}: {bug.title}")
ubuntu = launchpad.distributions["ubuntu"]
series = ubuntu.current_series.fullseriesname
for task in bug.bug_tasks:
# targeting to a series doesn't make the default task disappear,
# it just makes it useless
if task.bug_target_name == f"{package} ({series})":
our_task = task
break
if task.bug_target_name == f"{package} (Ubuntu)":
our_task = task
if our_task.assignee == launchpad.me:
print("Bug already assigned to you.")
return True
if our_task.assignee:
print(f"Currently assigned to {our_task.assignee.name}")
print("""Do you want to claim this bug? [yN] """, end="")
sys.stdout.flush()
response = sys.stdin.readline()
if response.strip().lower().startswith("y"):
our_task.assignee = launchpad.me
our_task.lp_save()
return True
return False
def create_excuses_bug(launchpad, package, version):
print("Will open a new bug")
bug = launchpad.bugs.createBug(
title=f"proposed-migration for {package} {version}",
tags=("update-excuse"),
target=f"https://api.launchpad.net/devel/ubuntu/+source/{package}",
description=f"{package} {version} is stuck in -proposed.",
)
task = bug.bug_tasks[0]
task.assignee = launchpad.me
task.lp_save()
print(f"Opening {bug.web_link} in browser")
webbrowser.open(bug.web_link)
return bug
def has_excuses_bugs(launchpad, package):
ubuntu = launchpad.distributions["ubuntu"]
pkg = ubuntu.getSourcePackage(name=package)
if not pkg:
raise ValueError(f"No such source package: {package}")
tasks = pkg.searchTasks(tags=["update-excuse"], order_by=["id"])
bugs = [task.bug for task in tasks]
if not bugs:
return False
if len(bugs) == 1:
print(f"There is 1 open update-excuse bug against {package}")
else:
print(f"There are {len(bugs)} open update-excuse bugs against {package}")
for bug in bugs:
if claim_excuses_bug(launchpad, bug, package):
return True
return True
def main():
parser = ArgumentParser()
parser.add_argument("-l", "--launchpad", dest="launchpad_instance", default="production")
parser.add_argument(
"-v", "--verbose", default=False, action="store_true", help="be more verbose"
)
parser.add_argument("package", nargs="?", help="act on this package only")
args = parser.parse_args()
args.launchpad = Launchpad.login_with("pm-helper", args.launchpad_instance, version="devel")
f = get_url(excuses_url, False)
with lzma.open(f) as lzma_f:
excuses = yaml.load(lzma_f, Loader=yaml.CSafeLoader)
if args.package:
try:
if not has_excuses_bugs(args.launchpad, args.package):
proposed_version = get_proposed_version(excuses, args.package)
if not proposed_version:
print(f"Package {args.package} not found in -proposed.")
sys.exit(1)
create_excuses_bug(args.launchpad, args.package, proposed_version)
except ValueError as e:
sys.stderr.write(f"{e}\n")
else:
pass # for now
if __name__ == "__main__":
sys.exit(main())

View File

@ -5,7 +5,10 @@
# #
# See pull-pkg # See pull-pkg
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main(distro='debian', pull='ddebs') PullPkg.main(distro="debian", pull="ddebs")

View File

@ -17,29 +17,32 @@
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
import optparse # pylint: disable=invalid-name
# pylint: enable=invalid-name
import argparse
import sys import sys
import debian.changelog import debian.changelog
from ubuntutools import getLogger
from ubuntutools.archive import DebianSourcePackage, DownloadError from ubuntutools.archive import DebianSourcePackage, DownloadError
from ubuntutools.config import UDTConfig from ubuntutools.config import UDTConfig
from ubuntutools.version import Version from ubuntutools.version import Version
from ubuntutools import getLogger
Logger = getLogger() Logger = getLogger()
def previous_version(package, version, distance): def previous_version(package, version, distance):
"Given an (extracted) package, determine the version distance versions ago" "Given an (extracted) package, determine the version distance versions ago"
upver = Version(version).upstream_version upver = Version(version).upstream_version
filename = '%s-%s/debian/changelog' % (package, upver) filename = f"{package}-{upver}/debian/changelog"
changelog_file = open(filename, 'r') changelog_file = open(filename, "r", encoding="utf-8")
changelog = debian.changelog.Changelog(changelog_file.read()) changelog = debian.changelog.Changelog(changelog_file.read())
changelog_file.close() changelog_file.close()
seen = 0 seen = 0
for entry in changelog: for entry in changelog:
if entry.distributions == 'UNRELEASED': if entry.distributions == "UNRELEASED":
continue continue
if seen == distance: if seen == distance:
return entry.version.full_version return entry.version.full_version
@ -48,69 +51,78 @@ def previous_version(package, version, distance):
def main(): def main():
parser = optparse.OptionParser('%prog [options] <package> <version> ' parser = argparse.ArgumentParser(usage="%(prog)s [options] <package> <version> [distance]")
'[distance]') parser.add_argument(
parser.add_option('-f', '--fetch', "-f",
dest='fetch_only', default=False, action='store_true', "--fetch",
help="Only fetch the source packages, don't diff.") dest="fetch_only",
parser.add_option('-d', '--debian-mirror', metavar='DEBIAN_MIRROR', default=False,
dest='debian_mirror', action="store_true",
help='Preferred Debian mirror ' help="Only fetch the source packages, don't diff.",
'(default: http://deb.debian.org/debian)') )
parser.add_option('-s', '--debsec-mirror', metavar='DEBSEC_MIRROR', parser.add_argument(
dest='debsec_mirror', "-d",
help='Preferred Debian Security mirror ' "--debian-mirror",
'(default: http://security.debian.org)') metavar="DEBIAN_MIRROR",
parser.add_option('--no-conf', dest="debian_mirror",
dest='no_conf', default=False, action='store_true', help="Preferred Debian mirror (default: http://deb.debian.org/debian)",
help="Don't read config files or environment variables") )
parser.add_argument(
"-s",
"--debsec-mirror",
metavar="DEBSEC_MIRROR",
dest="debsec_mirror",
help="Preferred Debian Security mirror (default: http://security.debian.org)",
)
parser.add_argument(
"--no-conf",
dest="no_conf",
default=False,
action="store_true",
help="Don't read config files or environment variables",
)
parser.add_argument("package", help=argparse.SUPPRESS)
parser.add_argument("version", help=argparse.SUPPRESS)
parser.add_argument("distance", default=1, type=int, nargs="?", help=argparse.SUPPRESS)
args = parser.parse_args()
opts, args = parser.parse_args() config = UDTConfig(args.no_conf)
if len(args) < 2: if args.debian_mirror is None:
parser.error('Must specify package and version') args.debian_mirror = config.get_value("DEBIAN_MIRROR")
elif len(args) > 3: if args.debsec_mirror is None:
parser.error('Too many arguments') args.debsec_mirror = config.get_value("DEBSEC_MIRROR")
package = args[0] mirrors = [args.debsec_mirror, args.debian_mirror]
version = args[1]
distance = int(args[2]) if len(args) > 2 else 1
config = UDTConfig(opts.no_conf) Logger.info("Downloading %s %s", args.package, args.version)
if opts.debian_mirror is None:
opts.debian_mirror = config.get_value('DEBIAN_MIRROR')
if opts.debsec_mirror is None:
opts.debsec_mirror = config.get_value('DEBSEC_MIRROR')
mirrors = [opts.debsec_mirror, opts.debian_mirror]
Logger.info('Downloading %s %s', package, version) newpkg = DebianSourcePackage(args.package, args.version, mirrors=mirrors)
newpkg = DebianSourcePackage(package, version, mirrors=mirrors)
try: try:
newpkg.pull() newpkg.pull()
except DownloadError as e: except DownloadError as e:
Logger.error('Failed to download: %s', str(e)) Logger.error("Failed to download: %s", str(e))
sys.exit(1) sys.exit(1)
newpkg.unpack() newpkg.unpack()
if opts.fetch_only: if args.fetch_only:
sys.exit(0) sys.exit(0)
oldversion = previous_version(package, version, distance) oldversion = previous_version(args.package, args.version, args.distance)
if not oldversion: if not oldversion:
Logger.error('No previous version could be found') Logger.error("No previous version could be found")
sys.exit(1) sys.exit(1)
Logger.info('Downloading %s %s', package, oldversion) Logger.info("Downloading %s %s", args.package, oldversion)
oldpkg = DebianSourcePackage(package, oldversion, mirrors=mirrors) oldpkg = DebianSourcePackage(args.package, oldversion, mirrors=mirrors)
try: try:
oldpkg.pull() oldpkg.pull()
except DownloadError as e: except DownloadError as e:
Logger.error('Failed to download: %s', str(e)) Logger.error("Failed to download: %s", str(e))
sys.exit(1) sys.exit(1)
Logger.info('file://' + oldpkg.debdiff(newpkg, diffstat=True)) Logger.info("file://%s", oldpkg.debdiff(newpkg, diffstat=True))
if __name__ == '__main__': if __name__ == "__main__":
try: try:
main() main()
except KeyboardInterrupt: except KeyboardInterrupt:
Logger.info('User abort.') Logger.info("User abort.")

View File

@ -5,7 +5,10 @@
# #
# See pull-pkg # See pull-pkg
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main(distro='debian', pull='debs') PullPkg.main(distro="debian", pull="debs")

View File

@ -5,7 +5,10 @@
# #
# See pull-pkg # See pull-pkg
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main(distro='debian', pull='source') PullPkg.main(distro="debian", pull="source")

View File

@ -5,7 +5,10 @@
# #
# See pull-pkg # See pull-pkg
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main(distro='debian', pull='udebs') PullPkg.main(distro="debian", pull="udebs")

View File

@ -5,7 +5,10 @@
# #
# See pull-pkg # See pull-pkg
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main(distro='ubuntu', pull='ddebs') PullPkg.main(distro="ubuntu", pull="ddebs")

View File

@ -5,7 +5,10 @@
# #
# See pull-pkg # See pull-pkg
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main(distro='ubuntu', pull='debs') PullPkg.main(distro="ubuntu", pull="debs")

View File

@ -5,7 +5,10 @@
# #
# See pull-pkg # See pull-pkg
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main(distro='ubuntu', pull='source') PullPkg.main(distro="ubuntu", pull="source")

View File

@ -5,7 +5,10 @@
# #
# See pull-pkg # See pull-pkg
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main(distro='ubuntu', pull='udebs') PullPkg.main(distro="ubuntu", pull="udebs")

View File

@ -23,7 +23,10 @@
# #
# ################################################################## # ##################################################################
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main() PullPkg.main()

View File

@ -6,7 +6,10 @@
# #
# See pull-pkg # See pull-pkg
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main(distro='ppa', pull='ddebs') PullPkg.main(distro="ppa", pull="ddebs")

View File

@ -6,7 +6,10 @@
# #
# See pull-pkg # See pull-pkg
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main(distro='ppa', pull='debs') PullPkg.main(distro="ppa", pull="debs")

View File

@ -6,7 +6,10 @@
# #
# See pull-pkg # See pull-pkg
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main(distro='ppa', pull='source') PullPkg.main(distro="ppa", pull="source")

View File

@ -6,7 +6,10 @@
# #
# See pull-pkg # See pull-pkg
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main(distro='ppa', pull='udebs') PullPkg.main(distro="ppa", pull="udebs")

View File

@ -5,7 +5,10 @@
# #
# See pull-pkg # See pull-pkg
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main(distro='uca', pull='ddebs') PullPkg.main(distro="uca", pull="ddebs")

View File

@ -5,7 +5,10 @@
# #
# See pull-pkg # See pull-pkg
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main(distro='uca', pull='debs') PullPkg.main(distro="uca", pull="debs")

View File

@ -5,7 +5,10 @@
# #
# See pull-pkg # See pull-pkg
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main(distro='uca', pull='source') PullPkg.main(distro="uca", pull="source")

View File

@ -5,7 +5,10 @@
# #
# See pull-pkg # See pull-pkg
# pylint: disable=invalid-name
# pylint: enable=invalid-name
from ubuntutools.pullpkg import PullPkg from ubuntutools.pullpkg import PullPkg
if __name__ == '__main__': if __name__ == "__main__":
PullPkg.main(distro='uca', pull='udebs') PullPkg.main(distro="uca", pull="udebs")

6
pyproject.toml Normal file
View File

@ -0,0 +1,6 @@
[tool.black]
line-length = 99
[tool.isort]
line_length = 99
profile = "black"

View File

@ -14,22 +14,20 @@
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
from collections import defaultdict import argparse
import optparse
import re
import sys import sys
from collections import defaultdict
import apt import apt
from distro_info import UbuntuDistroInfo from distro_info import UbuntuDistroInfo
from ubuntutools.config import UDTConfig
from ubuntutools.lp.lpapicache import Launchpad, Distribution
from ubuntutools.lp.udtexceptions import PackageNotFoundException
from ubuntutools.question import (YesNoQuestion, EditBugReport,
confirmation_prompt)
from ubuntutools.rdepends import query_rdepends, RDependsException
from ubuntutools import getLogger from ubuntutools import getLogger
from ubuntutools.config import UDTConfig
from ubuntutools.lp.lpapicache import Distribution, Launchpad
from ubuntutools.lp.udtexceptions import PackageNotFoundException
from ubuntutools.question import EditBugReport, YesNoQuestion, confirmation_prompt
from ubuntutools.rdepends import RDependsException, query_rdepends
Logger = getLogger() Logger = getLogger()
@ -40,16 +38,14 @@ class DestinationException(Exception):
def determine_destinations(source, destination): def determine_destinations(source, destination):
ubuntu_info = UbuntuDistroInfo() ubuntu_info = UbuntuDistroInfo()
if destination is None: if destination is None:
destination = ubuntu_info.stable() destination = ubuntu_info.lts()
if source not in ubuntu_info.all: if source not in ubuntu_info.all:
raise DestinationException("Source release %s does not exist" % source) raise DestinationException(f"Source release {source} does not exist")
if destination not in ubuntu_info.all: if destination not in ubuntu_info.all:
raise DestinationException("Destination release %s does not exist" raise DestinationException(f"Destination release {destination} does not exist")
% destination)
if destination not in ubuntu_info.supported(): if destination not in ubuntu_info.supported():
raise DestinationException("Destination release %s is not supported" raise DestinationException(f"Destination release {destination} is not supported")
% destination)
found = False found = False
destinations = [] destinations = []
@ -77,41 +73,36 @@ def determine_destinations(source, destination):
def disclaimer(): def disclaimer():
print("Ubuntu's backports are not for fixing bugs in stable releases, " print(
"but for bringing new features to older, stable releases.\n" "Ubuntu's backports are not for fixing bugs in stable releases, "
"See https://wiki.ubuntu.com/UbuntuBackports for the Ubuntu " "but for bringing new features to older, stable releases.\n"
"Backports policy and processes.\n" "See https://wiki.ubuntu.com/UbuntuBackports for the Ubuntu "
"See https://wiki.ubuntu.com/StableReleaseUpdates for the process " "Backports policy and processes.\n"
"for fixing bugs in stable releases.") "See https://wiki.ubuntu.com/StableReleaseUpdates for the process "
"for fixing bugs in stable releases."
)
confirmation_prompt() confirmation_prompt()
def check_existing(package, destinations): def check_existing(package):
"""Search for possible existing bug reports""" """Search for possible existing bug reports"""
# The LP bug search is indexed, not substring: distro = Distribution("ubuntu")
query = re.findall(r'[a-z]+', package) srcpkg = distro.getSourcePackage(name=package.getPackageName())
bugs = []
for release in destinations: bugs = srcpkg.searchTasks(
project_name = '{}-backports'.format(release) omit_duplicates=True,
try: search_text="[BPO]",
project = Launchpad.projects[project_name] status=["Incomplete", "New", "Confirmed", "Triaged", "In Progress", "Fix Committed"],
except KeyError: )
Logger.error("The backports tracking project '%s' doesn't seem to "
"exist. Please check the situation with the "
"backports team.", project_name)
sys.exit(1)
bugs += project.searchTasks(omit_duplicates=True,
search_text=query,
status=["Incomplete", "New", "Confirmed",
"Triaged", "In Progress",
"Fix Committed"])
if not bugs: if not bugs:
return return
Logger.info("There are existing bug reports that look similar to your " Logger.info(
"request. Please check before continuing:") "There are existing bug reports that look similar to your "
"request. Please check before continuing:"
)
for bug in sorted(set(bug_task.bug for bug_task in bugs)): for bug in sorted([bug_task.bug for bug_task in bugs], key=lambda bug: bug.id):
Logger.info(" * LP: #%-7i: %s %s", bug.id, bug.title, bug.web_link) Logger.info(" * LP: #%-7i: %s %s", bug.id, bug.title, bug.web_link)
confirmation_prompt() confirmation_prompt()
@ -122,9 +113,9 @@ def find_rdepends(releases, published_binaries):
# We want to display every pubilshed binary, even if it has no rdepends # We want to display every pubilshed binary, even if it has no rdepends
for binpkg in published_binaries: for binpkg in published_binaries:
intermediate[binpkg] intermediate[binpkg] # pylint: disable=pointless-statement
for arch in ('any', 'source'): for arch in ("any", "source"):
for release in releases: for release in releases:
for binpkg in published_binaries: for binpkg in published_binaries:
try: try:
@ -135,20 +126,20 @@ def find_rdepends(releases, published_binaries):
for relationship, rdeps in raw_rdeps.items(): for relationship, rdeps in raw_rdeps.items():
for rdep in rdeps: for rdep in rdeps:
# Ignore circular deps: # Ignore circular deps:
if rdep['Package'] in published_binaries: if rdep["Package"] in published_binaries:
continue continue
# arch==any queries return Reverse-Build-Deps: # arch==any queries return Reverse-Build-Deps:
if arch == 'any' and rdep.get('Architectures', []) == ['source']: if arch == "any" and rdep.get("Architectures", []) == ["source"]:
continue continue
intermediate[binpkg][rdep['Package']].append((release, relationship)) intermediate[binpkg][rdep["Package"]].append((release, relationship))
output = [] output = []
for binpkg, rdeps in intermediate.items(): for binpkg, rdeps in intermediate.items():
output += ['', binpkg, '-' * len(binpkg)] output += ["", binpkg, "-" * len(binpkg)]
for pkg, appearences in rdeps.items(): for pkg, appearences in rdeps.items():
output += ['* %s' % pkg] output += [f"* {pkg}"]
for release, relationship in appearences: for release, relationship in appearences:
output += [' [ ] %s (%s)' % (release, relationship)] output += [f" [ ] {release} ({relationship})"]
found_any = sum(len(rdeps) for rdeps in intermediate.values()) found_any = sum(len(rdeps) for rdeps in intermediate.values())
if found_any: if found_any:
@ -163,8 +154,8 @@ def find_rdepends(releases, published_binaries):
"package currently in the release still works with the new " "package currently in the release still works with the new "
"%(package)s installed. " "%(package)s installed. "
"Reverse- Recommends, Suggests, and Enhances don't need to be " "Reverse- Recommends, Suggests, and Enhances don't need to be "
"tested, and are listed for completeness-sake." "tested, and are listed for completeness-sake.",
] + output ] + output
else: else:
output = ["No reverse dependencies"] output = ["No reverse dependencies"]
@ -172,146 +163,164 @@ def find_rdepends(releases, published_binaries):
def locate_package(package, distribution): def locate_package(package, distribution):
archive = Distribution('ubuntu').getArchive() archive = Distribution("ubuntu").getArchive()
for pass_ in ('source', 'binary'): try:
try: package_spph = archive.getSourcePackage(package, distribution)
package_spph = archive.getSourcePackage(package, distribution) return package_spph
return package_spph except PackageNotFoundException as e:
except PackageNotFoundException as e:
if pass_ == 'binary':
Logger.error(str(e))
sys.exit(1)
try: try:
apt_pkg = apt.Cache()[package] apt_pkg = apt.Cache()[package]
except KeyError: except KeyError:
continue Logger.error(str(e))
sys.exit(1)
package = apt_pkg.candidate.source_name package = apt_pkg.candidate.source_name
Logger.info("Binary package specified, considering its source " Logger.info(
"package instead: %s", package) "Binary package specified, considering its source package instead: %s", package
)
return None
def request_backport(package_spph, source, destinations): def request_backport(package_spph, source, destinations):
published_binaries = set() published_binaries = set()
for bpph in package_spph.getBinaries(): for bpph in package_spph.getBinaries():
published_binaries.add(bpph.getPackageName()) published_binaries.add(bpph.getPackageName())
if not published_binaries: if not published_binaries:
Logger.error("%s (%s) has no published binaries in %s. ", Logger.error(
package_spph.getPackageName(), package_spph.getVersion(), "%s (%s) has no published binaries in %s. ",
source) package_spph.getPackageName(),
Logger.info("Is it stuck in bin-NEW? It can't be backported until " package_spph.getVersion(),
"the binaries have been accepted.") source,
)
Logger.info(
"Is it stuck in bin-NEW? It can't be backported until "
"the binaries have been accepted."
)
sys.exit(1) sys.exit(1)
testing = [] testing = ["[Testing]", ""]
testing += ["You can test-build the backport in your PPA with "
"backportpackage:"]
testing += ["$ backportpackage -u ppa:<lp username>/<ppa name> "
"-s %s -d %s %s"
% (source, dest, package_spph.getPackageName())
for dest in destinations]
testing += [""]
for dest in destinations: for dest in destinations:
testing += ['* %s:' % dest] testing += [f" * {dest.capitalize()}:"]
testing += ["[ ] Package builds without modification"] testing += [" [ ] Package builds without modification"]
testing += ["[ ] %s installs cleanly and runs" % binary testing += [f" [ ] {binary} installs cleanly and runs" for binary in published_binaries]
for binary in published_binaries]
subst = { subst = {
'package': package_spph.getPackageName(), "package": package_spph.getPackageName(),
'version': package_spph.getVersion(), "version": package_spph.getVersion(),
'component': package_spph.getComponent(), "component": package_spph.getComponent(),
'source': package_spph.getSeriesAndPocket(), "source": package_spph.getSeriesAndPocket(),
'destinations': ', '.join(destinations), "destinations": ", ".join(destinations),
} }
subject = ("Please backport %(package)s %(version)s (%(component)s) " subject = "[BPO] %(package)s %(version)s to %(destinations)s" % subst
"from %(source)s" % subst) body = (
body = ('\n'.join( "\n".join(
[ [
"Please backport %(package)s %(version)s (%(component)s) " "[Impact]",
"from %(source)s to %(destinations)s.", "",
"", " * Justification for backporting the new version to the stable release.",
"Reason for the backport:", "",
"========================", "[Scope]",
">>> Enter your reasoning here <<<", "",
"", " * List the Ubuntu release you will backport from,"
"Testing:", " and the specific package version.",
"========", "",
"Mark off items in the checklist [X] as you test them, " " * List the Ubuntu release(s) you will backport to.",
"but please leave the checklist so that backporters can quickly " "",
"evaluate the state of testing.", "[Other Info]",
"" "",
" * Anything else you think is useful to include",
"",
] ]
+ testing + testing
+ [""] + [""]
+ find_rdepends(destinations, published_binaries) + find_rdepends(destinations, published_binaries)
+ [""]) % subst) + [""]
)
% subst
)
editor = EditBugReport(subject, body) editor = EditBugReport(subject, body)
editor.edit() editor.edit()
subject, body = editor.get_report() subject, body = editor.get_report()
Logger.info('The final report is:\nSummary: %s\nDescription:\n%s\n', Logger.info("The final report is:\nSummary: %s\nDescription:\n%s\n", subject, body)
subject, body)
if YesNoQuestion().ask("Request this backport", "yes") == "no": if YesNoQuestion().ask("Request this backport", "yes") == "no":
sys.exit(1) sys.exit(1)
targets = [Launchpad.projects['%s-backports' % destination] distro = Distribution("ubuntu")
for destination in destinations] pkgname = package_spph.getPackageName()
bug = Launchpad.bugs.createBug(title=subject, description=body,
target=targets[0]) bug = Launchpad.bugs.createBug(
for target in targets[1:]: title=subject, description=body, target=distro.getSourcePackage(name=pkgname)
bug.addTask(target=target) )
bug.subscribe(person=Launchpad.people["ubuntu-backporters"])
for dest in destinations:
series = distro.getSeries(dest)
try:
bug.addTask(target=series.getSourcePackage(name=pkgname))
except Exception: # pylint: disable=broad-except
break
Logger.info("Backport request filed as %s", bug.web_link) Logger.info("Backport request filed as %s", bug.web_link)
def main(): def main():
parser = optparse.OptionParser('%prog [options] package') parser = argparse.ArgumentParser(usage="%(prog)s [options] package")
parser.add_option('-d', '--destination', metavar='DEST', parser.add_argument(
help='Backport to DEST release and necessary ' "-d",
'intermediate releases ' "--destination",
'(default: current stable release)') metavar="DEST",
parser.add_option('-s', '--source', metavar='SOURCE', help="Backport to DEST release and necessary "
help='Backport from SOURCE release ' "intermediate releases "
'(default: current devel release)') "(default: current LTS release)",
parser.add_option('-l', '--lpinstance', metavar='INSTANCE', default=None, )
help='Launchpad instance to connect to ' parser.add_argument(
'(default: production).') "-s",
parser.add_option('--no-conf', action='store_true', "--source",
dest='no_conf', default=False, metavar="SOURCE",
help="Don't read config files or environment variables") help="Backport from SOURCE release (default: current devel release)",
options, args = parser.parse_args() )
parser.add_argument(
"-l",
"--lpinstance",
metavar="INSTANCE",
default=None,
help="Launchpad instance to connect to (default: production).",
)
parser.add_argument(
"--no-conf",
action="store_true",
dest="no_conf",
default=False,
help="Don't read config files or environment variables",
)
parser.add_argument("package", help=argparse.SUPPRESS)
args = parser.parse_args()
if len(args) != 1: config = UDTConfig(args.no_conf)
parser.error("One (and only one) package must be specified")
package = args[0]
config = UDTConfig(options.no_conf) if args.lpinstance is None:
args.lpinstance = config.get_value("LPINSTANCE")
Launchpad.login(args.lpinstance)
if options.lpinstance is None: if args.source is None:
options.lpinstance = config.get_value('LPINSTANCE') args.source = Distribution("ubuntu").getDevelopmentSeries().name
Launchpad.login(options.lpinstance)
if options.source is None:
options.source = Distribution('ubuntu').getDevelopmentSeries().name
try: try:
destinations = determine_destinations(options.source, destinations = determine_destinations(args.source, args.destination)
options.destination)
except DestinationException as e: except DestinationException as e:
Logger.error(str(e)) Logger.error(str(e))
sys.exit(1) sys.exit(1)
disclaimer() disclaimer()
check_existing(package, destinations) package_spph = locate_package(args.package, args.source)
package_spph = locate_package(package, options.source) check_existing(package_spph)
request_backport(package_spph, options.source, destinations) request_backport(package_spph, args.source, destinations)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -26,19 +26,19 @@
# #
# ################################################################## # ##################################################################
import optparse import argparse
import os import os
import sys import sys
from distro_info import UbuntuDistroInfo from distro_info import UbuntuDistroInfo
from ubuntutools import getLogger
from ubuntutools.config import UDTConfig, ubu_email from ubuntutools.config import UDTConfig, ubu_email
from ubuntutools.lp import udtexceptions from ubuntutools.lp import udtexceptions
from ubuntutools.misc import require_utf8 from ubuntutools.misc import require_utf8
from ubuntutools.question import confirmation_prompt, EditBugReport from ubuntutools.question import EditBugReport, confirmation_prompt
from ubuntutools.version import Version from ubuntutools.version import Version
from ubuntutools import getLogger
Logger = getLogger() Logger = getLogger()
# #
@ -48,170 +48,190 @@ Logger = getLogger()
def main(): def main():
# Our usage options. # Our usage options.
usage = ('Usage: %prog [options] ' usage = "%(prog)s [options] <source package> [<target release> [base version]]"
'<source package> [<target release> [base version]]') parser = argparse.ArgumentParser(usage=usage)
parser = optparse.OptionParser(usage)
parser.add_option('-d', type='string', parser.add_argument(
dest='dist', default='unstable', "-d", dest="dist", default="unstable", help="Debian distribution to sync from."
help='Debian distribution to sync from.') )
parser.add_option('-k', type='string', parser.add_argument(
dest='keyid', default=None, "-k",
help='GnuPG key ID to use for signing report ' dest="keyid",
'(only used when emailing the sync request).') default=None,
parser.add_option('-n', action='store_true', help="GnuPG key ID to use for signing report "
dest='newpkg', default=False, "(only used when emailing the sync request).",
help='Whether package to sync is a new package in ' )
'Ubuntu.') parser.add_argument(
parser.add_option('--email', action='store_true', default=False, "-n",
help='Use a PGP-signed email for filing the sync ' action="store_true",
'request, rather than the LP API.') dest="newpkg",
parser.add_option('--lp', dest='deprecated_lp_flag', default=False,
action='store_true', default=False, help="Whether package to sync is a new package in Ubuntu.",
help=optparse.SUPPRESS_HELP) )
parser.add_option('-l', '--lpinstance', metavar='INSTANCE', parser.add_argument(
dest='lpinstance', default=None, "--email",
help='Launchpad instance to connect to ' action="store_true",
'(default: production).') default=False,
parser.add_option('-s', action='store_true', help="Use a PGP-signed email for filing the sync request, rather than the LP API.",
dest='sponsorship', default=False, )
help='Force sponsorship') parser.add_argument(
parser.add_option('-C', action='store_true', "--lp",
dest='missing_changelog_ok', default=False, dest="deprecated_lp_flag",
help='Allow changelog to be manually filled in ' action="store_true",
'when missing') default=False,
parser.add_option('-e', action='store_true', help=argparse.SUPPRESS,
dest='ffe', default=False, )
help='Use this after FeatureFreeze for non-bug fix ' parser.add_argument(
'syncs, changes default subscription to the ' "-l",
'appropriate release team.') "--lpinstance",
parser.add_option('--no-conf', action='store_true', metavar="INSTANCE",
dest='no_conf', default=False, dest="lpinstance",
help="Don't read config files or environment variables") default=None,
help="Launchpad instance to connect to (default: production).",
(options, args) = parser.parse_args() )
parser.add_argument(
if not len(args): "-s", action="store_true", dest="sponsorship", default=False, help="Force sponsorship"
parser.print_help() )
sys.exit(1) parser.add_argument(
"-C",
action="store_true",
dest="missing_changelog_ok",
default=False,
help="Allow changelog to be manually filled in when missing",
)
parser.add_argument(
"-e",
action="store_true",
dest="ffe",
default=False,
help="Use this after FeatureFreeze for non-bug fix "
"syncs, changes default subscription to the "
"appropriate release team.",
)
parser.add_argument(
"--no-conf",
action="store_true",
dest="no_conf",
default=False,
help="Don't read config files or environment variables",
)
parser.add_argument("source_package", help=argparse.SUPPRESS)
parser.add_argument("release", nargs="?", help=argparse.SUPPRESS)
parser.add_argument("base_version", nargs="?", type=Version, help=argparse.SUPPRESS)
args = parser.parse_args()
require_utf8() require_utf8()
config = UDTConfig(options.no_conf) config = UDTConfig(args.no_conf)
if options.deprecated_lp_flag: if args.deprecated_lp_flag:
Logger.info("The --lp flag is now default, ignored.") Logger.info("The --lp flag is now default, ignored.")
if options.email: if args.email:
options.lpapi = False args.lpapi = False
else: else:
options.lpapi = config.get_value('USE_LPAPI', default=True, args.lpapi = config.get_value("USE_LPAPI", default=True, boolean=True)
boolean=True) if args.lpinstance is None:
if options.lpinstance is None: args.lpinstance = config.get_value("LPINSTANCE")
options.lpinstance = config.get_value('LPINSTANCE')
if options.keyid is None: if args.keyid is None:
options.keyid = config.get_value('KEYID') args.keyid = config.get_value("KEYID")
if not options.lpapi: if not args.lpapi:
if options.lpinstance == 'production': if args.lpinstance == "production":
bug_mail_domain = 'bugs.launchpad.net' bug_mail_domain = "bugs.launchpad.net"
elif options.lpinstance == 'staging': elif args.lpinstance == "staging":
bug_mail_domain = 'bugs.staging.launchpad.net' bug_mail_domain = "bugs.staging.launchpad.net"
else: else:
Logger.error('Error: Unknown launchpad instance: %s' Logger.error("Error: Unknown launchpad instance: %s", args.lpinstance)
% options.lpinstance)
sys.exit(1) sys.exit(1)
mailserver_host = config.get_value('SMTP_SERVER', mailserver_host = config.get_value(
default=None, "SMTP_SERVER", default=None, compat_keys=["UBUSMTP", "DEBSMTP"]
compat_keys=['UBUSMTP', 'DEBSMTP']) )
if not options.lpapi and not mailserver_host: if not args.lpapi and not mailserver_host:
try: try:
import DNS import DNS # pylint: disable=import-outside-toplevel
DNS.DiscoverNameServers() DNS.DiscoverNameServers()
mxlist = DNS.mxlookup(bug_mail_domain) mxlist = DNS.mxlookup(bug_mail_domain)
firstmx = mxlist[0] firstmx = mxlist[0]
mailserver_host = firstmx[1] mailserver_host = firstmx[1]
except ImportError: except ImportError:
Logger.error('Please install python-dns to support ' Logger.error("Please install python-dns to support Launchpad mail server lookup.")
'Launchpad mail server lookup.')
sys.exit(1) sys.exit(1)
mailserver_port = config.get_value('SMTP_PORT', default=25, mailserver_port = config.get_value(
compat_keys=['UBUSMTP_PORT', "SMTP_PORT", default=25, compat_keys=["UBUSMTP_PORT", "DEBSMTP_PORT"]
'DEBSMTP_PORT']) )
mailserver_user = config.get_value('SMTP_USER', mailserver_user = config.get_value("SMTP_USER", compat_keys=["UBUSMTP_USER", "DEBSMTP_USER"])
compat_keys=['UBUSMTP_USER', mailserver_pass = config.get_value("SMTP_PASS", compat_keys=["UBUSMTP_PASS", "DEBSMTP_PASS"])
'DEBSMTP_USER'])
mailserver_pass = config.get_value('SMTP_PASS',
compat_keys=['UBUSMTP_PASS',
'DEBSMTP_PASS'])
# import the needed requestsync module # import the needed requestsync module
if options.lpapi: # pylint: disable=import-outside-toplevel
from ubuntutools.requestsync.lp import (check_existing_reports, if args.lpapi:
get_debian_srcpkg,
get_ubuntu_srcpkg,
get_ubuntu_delta_changelog,
need_sponsorship, post_bug)
from ubuntutools.lp.lpapicache import Distribution, Launchpad from ubuntutools.lp.lpapicache import Distribution, Launchpad
from ubuntutools.requestsync.lp import (
check_existing_reports,
get_debian_srcpkg,
get_ubuntu_delta_changelog,
get_ubuntu_srcpkg,
need_sponsorship,
post_bug,
)
# See if we have LP credentials and exit if we don't - # See if we have LP credentials and exit if we don't -
# cannot continue in this case # cannot continue in this case
try: try:
# devel for changelogUrl() # devel for changelogUrl()
Launchpad.login(service=options.lpinstance, api_version='devel') Launchpad.login(service=args.lpinstance, api_version="devel")
except IOError: except IOError:
sys.exit(1) sys.exit(1)
else: else:
from ubuntutools.requestsync.mail import (check_existing_reports, from ubuntutools.requestsync.mail import (
get_debian_srcpkg, check_existing_reports,
get_ubuntu_srcpkg, get_debian_srcpkg,
get_ubuntu_delta_changelog, get_ubuntu_delta_changelog,
mail_bug, need_sponsorship) get_ubuntu_srcpkg,
if not any(x in os.environ for x in ('UBUMAIL', 'DEBEMAIL', 'EMAIL')): mail_bug,
Logger.error('The environment variable UBUMAIL, DEBEMAIL or EMAIL needs ' need_sponsorship,
'to be set to let this script mail the sync request.') )
if not any(x in os.environ for x in ("UBUMAIL", "DEBEMAIL", "EMAIL")):
Logger.error(
"The environment variable UBUMAIL, DEBEMAIL or EMAIL needs "
"to be set to let this script mail the sync request."
)
sys.exit(1) sys.exit(1)
newsource = options.newpkg newsource = args.newpkg
sponsorship = options.sponsorship sponsorship = args.sponsorship
distro = options.dist distro = args.dist
ffe = options.ffe ffe = args.ffe
lpapi = options.lpapi lpapi = args.lpapi
need_interaction = False need_interaction = False
force_base_version = None srcpkg = args.source_package
srcpkg = args[0]
if len(args) == 1: if not args.release:
if lpapi: if lpapi:
release = Distribution('ubuntu').getDevelopmentSeries().name args.release = Distribution("ubuntu").getDevelopmentSeries().name
else: else:
ubu_info = UbuntuDistroInfo() ubu_info = UbuntuDistroInfo()
release = ubu_info.devel() args.release = ubu_info.devel()
Logger.warning('Target release missing - assuming %s' % release) Logger.warning("Target release missing - assuming %s", args.release)
elif len(args) == 2:
release = args[1]
elif len(args) == 3:
release = args[1]
force_base_version = Version(args[2])
else:
Logger.error('Too many arguments.')
parser.print_help()
sys.exit(1)
# Get the current Ubuntu source package # Get the current Ubuntu source package
try: try:
ubuntu_srcpkg = get_ubuntu_srcpkg(srcpkg, release, 'Proposed') ubuntu_srcpkg = get_ubuntu_srcpkg(srcpkg, args.release, "Proposed")
ubuntu_version = Version(ubuntu_srcpkg.getVersion()) ubuntu_version = Version(ubuntu_srcpkg.getVersion())
ubuntu_component = ubuntu_srcpkg.getComponent() ubuntu_component = ubuntu_srcpkg.getComponent()
newsource = False # override the -n flag newsource = False # override the -n flag
except udtexceptions.PackageNotFoundException: except udtexceptions.PackageNotFoundException:
ubuntu_srcpkg = None ubuntu_srcpkg = None
ubuntu_version = Version('~') ubuntu_version = Version("~")
ubuntu_component = None # Set after getting the Debian info ubuntu_component = None # Set after getting the Debian info
if not newsource: if not newsource:
Logger.info("'%s' doesn't exist in 'Ubuntu %s'." % (srcpkg, release)) Logger.info("'%s' doesn't exist in 'Ubuntu %s'.", srcpkg, args.release)
Logger.info("Do you want to sync a new package?") Logger.info("Do you want to sync a new package?")
confirmation_prompt() confirmation_prompt()
newsource = True newsource = True
@ -232,15 +252,16 @@ def main():
sys.exit(1) sys.exit(1)
if ubuntu_component is None: if ubuntu_component is None:
if debian_component == 'main': if debian_component == "main":
ubuntu_component = 'universe' ubuntu_component = "universe"
else: else:
ubuntu_component = 'multiverse' ubuntu_component = "multiverse"
# Stop if Ubuntu has already the version from Debian or a newer version # Stop if Ubuntu has already the version from Debian or a newer version
if (ubuntu_version >= debian_version) and options.lpapi: if (ubuntu_version >= debian_version) and args.lpapi:
# try rmadison # try rmadison
import ubuntutools.requestsync.mail import ubuntutools.requestsync.mail # pylint: disable=import-outside-toplevel
try: try:
debian_srcpkg = ubuntutools.requestsync.mail.get_debian_srcpkg(srcpkg, distro) debian_srcpkg = ubuntutools.requestsync.mail.get_debian_srcpkg(srcpkg, distro)
debian_version = Version(debian_srcpkg.getVersion()) debian_version = Version(debian_srcpkg.getVersion())
@ -250,72 +271,80 @@ def main():
sys.exit(1) sys.exit(1)
if ubuntu_version == debian_version: if ubuntu_version == debian_version:
Logger.error('The versions in Debian and Ubuntu are the ' Logger.error(
'same already (%s). Aborting.' % ubuntu_version) "The versions in Debian and Ubuntu are the same already (%s). Aborting.",
ubuntu_version,
)
sys.exit(1) sys.exit(1)
if ubuntu_version > debian_version: if ubuntu_version > debian_version:
Logger.error('The version in Ubuntu (%s) is newer than ' Logger.error(
'the version in Debian (%s). Aborting.' "The version in Ubuntu (%s) is newer than the version in Debian (%s). Aborting.",
% (ubuntu_version, debian_version)) ubuntu_version,
debian_version,
)
sys.exit(1) sys.exit(1)
# -s flag not specified - check if we do need sponsorship # -s flag not specified - check if we do need sponsorship
if not sponsorship: if not sponsorship:
sponsorship = need_sponsorship(srcpkg, ubuntu_component, release) sponsorship = need_sponsorship(srcpkg, ubuntu_component, args.release)
if not sponsorship and not ffe: if not sponsorship and not ffe:
Logger.error('Consider using syncpackage(1) for syncs that ' Logger.error(
'do not require feature freeze exceptions.') "Consider using syncpackage(1) for syncs that "
"do not require feature freeze exceptions."
)
# Check for existing package reports # Check for existing package reports
if not newsource: if not newsource:
check_existing_reports(srcpkg) check_existing_reports(srcpkg)
# Generate bug report # Generate bug report
pkg_to_sync = ('%s %s (%s) from Debian %s (%s)' pkg_to_sync = (
% (srcpkg, debian_version, ubuntu_component, f"{srcpkg} {debian_version} ({ubuntu_component})"
distro, debian_component)) f" from Debian {distro} ({debian_component})"
title = "Sync %s" % pkg_to_sync )
title = f"Sync {pkg_to_sync}"
if ffe: if ffe:
title = "FFe: " + title title = "FFe: " + title
report = "Please sync %s\n\n" % pkg_to_sync report = f"Please sync {pkg_to_sync}\n\n"
if 'ubuntu' in str(ubuntu_version): if "ubuntu" in str(ubuntu_version):
need_interaction = True need_interaction = True
Logger.info('Changes have been made to the package in Ubuntu.') Logger.info("Changes have been made to the package in Ubuntu.")
Logger.info('Please edit the report and give an explanation.') Logger.info("Please edit the report and give an explanation.")
Logger.info('Not saving the report file will abort the request.') Logger.info("Not saving the report file will abort the request.")
report += ('Explanation of the Ubuntu delta and why it can be ' report += (
'dropped:\n%s\n>>> ENTER_EXPLANATION_HERE <<<\n\n' f"Explanation of the Ubuntu delta and why it can be dropped:\n"
% get_ubuntu_delta_changelog(ubuntu_srcpkg)) f"{get_ubuntu_delta_changelog(ubuntu_srcpkg)}\n>>> ENTER_EXPLANATION_HERE <<<\n\n"
)
if ffe: if ffe:
need_interaction = True need_interaction = True
Logger.info('To approve FeatureFreeze exception, you need to state') Logger.info("To approve FeatureFreeze exception, you need to state")
Logger.info('the reason why you feel it is necessary.') Logger.info("the reason why you feel it is necessary.")
Logger.info('Not saving the report file will abort the request.') Logger.info("Not saving the report file will abort the request.")
report += ('Explanation of FeatureFreeze exception:\n' report += "Explanation of FeatureFreeze exception:\n>>> ENTER_EXPLANATION_HERE <<<\n\n"
'>>> ENTER_EXPLANATION_HERE <<<\n\n')
if need_interaction: if need_interaction:
confirmation_prompt() confirmation_prompt()
base_version = force_base_version or ubuntu_version base_version = args.base_version or ubuntu_version
if newsource: if newsource:
report += 'All changelog entries:\n\n' report += "All changelog entries:\n\n"
else: else:
report += ('Changelog entries since current %s version %s:\n\n' report += f"Changelog entries since current {args.release} version {ubuntu_version}:\n\n"
% (release, ubuntu_version))
changelog = debian_srcpkg.getChangelog(since_version=base_version) changelog = debian_srcpkg.getChangelog(since_version=base_version)
if not changelog: if not changelog:
if not options.missing_changelog_ok: if not args.missing_changelog_ok:
Logger.error("Did not retrieve any changelog entries. " Logger.error(
"Do you need to specify '-C'? " "Did not retrieve any changelog entries. "
"Was the package recently uploaded? (check " "Do you need to specify '-C'? "
"http://packages.debian.org/changelogs/)") "Was the package recently uploaded? (check "
"http://packages.debian.org/changelogs/)"
)
sys.exit(1) sys.exit(1)
else: else:
need_interaction = True need_interaction = True
@ -326,36 +355,49 @@ def main():
editor.edit(optional=not need_interaction) editor.edit(optional=not need_interaction)
title, report = editor.get_report() title, report = editor.get_report()
if 'XXX FIXME' in report: if "XXX FIXME" in report:
Logger.error("changelog boilerplate found in report, " Logger.error(
"please manually add changelog when using '-C'") "changelog boilerplate found in report, "
"please manually add changelog when using '-C'"
)
sys.exit(1) sys.exit(1)
# bug status and bug subscriber # bug status and bug subscriber
status = 'confirmed' status = "confirmed"
subscribe = 'ubuntu-archive' subscribe = "ubuntu-archive"
if sponsorship: if sponsorship:
status = 'new' status = "new"
subscribe = 'ubuntu-sponsors' subscribe = "ubuntu-sponsors"
if ffe: if ffe:
status = 'new' status = "new"
subscribe = 'ubuntu-release' subscribe = "ubuntu-release"
srcpkg = not newsource and srcpkg or None srcpkg = None if newsource else srcpkg
if lpapi: if lpapi:
# Map status to the values expected by LP API # Map status to the values expected by LP API
mapping = {'new': 'New', 'confirmed': 'Confirmed'} mapping = {"new": "New", "confirmed": "Confirmed"}
# Post sync request using LP API # Post sync request using LP API
post_bug(srcpkg, subscribe, mapping[status], title, report) post_bug(srcpkg, subscribe, mapping[status], title, report)
else: else:
email_from = ubu_email(export=False)[1] email_from = ubu_email(export=False)[1]
# Mail sync request # Mail sync request
mail_bug(srcpkg, subscribe, status, title, report, bug_mail_domain, mail_bug(
options.keyid, email_from, mailserver_host, mailserver_port, srcpkg,
mailserver_user, mailserver_pass) subscribe,
status,
title,
report,
bug_mail_domain,
args.keyid,
email_from,
mailserver_host,
mailserver_port,
mailserver_user,
mailserver_pass,
)
if __name__ == '__main__': if __name__ == "__main__":
try: try:
main() main()
except KeyboardInterrupt: except KeyboardInterrupt:

View File

@ -1,5 +1,6 @@
python-debian python-debian
python-debianbts python-debianbts
dateutil
distro-info distro-info
httplib2 httplib2
launchpadlib launchpadlib

View File

@ -14,16 +14,18 @@
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# pylint: disable=invalid-name
# pylint: enable=invalid-name
import argparse import argparse
import sys import sys
from distro_info import DistroDataOutdated from distro_info import DistroDataOutdated
from ubuntutools.misc import (system_distribution, vendor_to_distroinfo,
codename_to_distribution)
from ubuntutools.rdepends import query_rdepends, RDependsException
from ubuntutools import getLogger from ubuntutools import getLogger
from ubuntutools.misc import codename_to_distribution, system_distribution, vendor_to_distroinfo
from ubuntutools.rdepends import RDependsException, query_rdepends
Logger = getLogger() Logger = getLogger()
DEFAULT_MAX_DEPTH = 10 # We want avoid any infinite loop... DEFAULT_MAX_DEPTH = 10 # We want avoid any infinite loop...
@ -35,77 +37,107 @@ def main():
default_release = system_distro_info.devel() default_release = system_distro_info.devel()
except DistroDataOutdated as e: except DistroDataOutdated as e:
Logger.warning(e) Logger.warning(e)
default_release = 'unstable' default_release = "unstable"
description = ("List reverse-dependencies of package. " description = (
"If the package name is prefixed with src: then the " "List reverse-dependencies of package. "
"reverse-dependencies of all the binary packages that " "If the package name is prefixed with src: then the "
"the specified source package builds will be listed.") "reverse-dependencies of all the binary packages that "
"the specified source package builds will be listed."
)
parser = argparse.ArgumentParser(description=description) parser = argparse.ArgumentParser(description=description)
parser.add_argument('-r', '--release', default=default_release, parser.add_argument(
help='Query dependencies in RELEASE. ' "-r",
'Default: %s' % default_release) "--release",
parser.add_argument('-R', '--without-recommends', action='store_false', default=default_release,
dest='recommends', help="Query dependencies in RELEASE. Default: %(default)s",
help='Only consider Depends relationships, ' )
'not Recommends') parser.add_argument(
parser.add_argument('-s', '--with-suggests', action='store_true', "-R",
help='Also consider Suggests relationships') "--without-recommends",
parser.add_argument('-b', '--build-depends', action='store_true', action="store_false",
help='Query build dependencies (synonym for --arch=source)') dest="recommends",
parser.add_argument('-a', '--arch', default='any', help="Only consider Depends relationships, not Recommends",
help='Query dependencies in ARCH. Default: any') )
parser.add_argument('-c', '--component', action='append', parser.add_argument(
help='Only consider reverse-dependencies in COMPONENT. ' "-s", "--with-suggests", action="store_true", help="Also consider Suggests relationships"
'Can be specified multiple times. Default: all') )
parser.add_argument('-l', '--list', action='store_true', parser.add_argument(
help='Display a simple, machine-readable list') "-b",
parser.add_argument('-u', '--service-url', metavar='URL', "--build-depends",
dest='server', default=None, action="store_true",
help='Reverse Dependencies webservice URL. ' help="Query build dependencies (synonym for --arch=source)",
'Default: UbuntuWire') )
parser.add_argument('-x', '--recursive', action='store_true', parser.add_argument(
help='Consider to find reverse dependencies recursively.') "-a", "--arch", default="any", help="Query dependencies in ARCH. Default: any"
parser.add_argument('-d', '--recursive-depth', type=int, )
default=DEFAULT_MAX_DEPTH, parser.add_argument(
help='If recusive, you can specify the depth.') "-c",
parser.add_argument('package') "--component",
action="append",
help="Only consider reverse-dependencies in COMPONENT. "
"Can be specified multiple times. Default: all",
)
parser.add_argument(
"-l", "--list", action="store_true", help="Display a simple, machine-readable list"
)
parser.add_argument(
"-u",
"--service-url",
metavar="URL",
dest="server",
default=None,
help="Reverse Dependencies webservice URL. Default: UbuntuWire",
)
parser.add_argument(
"-x",
"--recursive",
action="store_true",
help="Consider to find reverse dependencies recursively.",
)
parser.add_argument(
"-d",
"--recursive-depth",
type=int,
default=DEFAULT_MAX_DEPTH,
help="If recusive, you can specify the depth.",
)
parser.add_argument("package")
options = parser.parse_args() options = parser.parse_args()
opts = {} opts = {}
if options.server is not None: if options.server is not None:
opts['server'] = options.server opts["server"] = options.server
# Convert unstable/testing aliases to codenames: # Convert unstable/testing aliases to codenames:
distribution = codename_to_distribution(options.release) distribution = codename_to_distribution(options.release)
if not distribution: if not distribution:
parser.error('Unknown release codename %s' % options.release) parser.error(f"Unknown release codename {options.release}")
distro_info = vendor_to_distroinfo(distribution)() distro_info = vendor_to_distroinfo(distribution)()
try: try:
options.release = distro_info.codename(options.release, options.release = distro_info.codename(options.release, default=options.release)
default=options.release)
except DistroDataOutdated: except DistroDataOutdated:
# We already logged a warning # We already logged a warning
pass pass
if options.build_depends: if options.build_depends:
options.arch = 'source' options.arch = "source"
if options.arch == 'source': if options.arch == "source":
fields = [ fields = [
'Reverse-Build-Depends', "Reverse-Build-Depends",
'Reverse-Build-Depends-Indep', "Reverse-Build-Depends-Indep",
'Reverse-Build-Depends-Arch', "Reverse-Build-Depends-Arch",
'Reverse-Testsuite-Triggers', "Reverse-Testsuite-Triggers",
] ]
else: else:
fields = ['Reverse-Depends'] fields = ["Reverse-Depends"]
if options.recommends: if options.recommends:
fields.append('Reverse-Recommends') fields.append("Reverse-Recommends")
if options.with_suggests: if options.with_suggests:
fields.append('Reverse-Suggests') fields.append("Reverse-Suggests")
def build_results(package, result, fields, component, recursive): def build_results(package, result, fields, component, recursive):
try: try:
@ -119,9 +151,9 @@ def main():
if fields: if fields:
data = {k: v for k, v in data.items() if k in fields} data = {k: v for k, v in data.items() if k in fields}
if component: if component:
data = {k: [rdep for rdep in v data = {
if rdep['Component'] in component] k: [rdep for rdep in v if rdep["Component"] in component] for k, v in data.items()
for k, v in data.items()} }
data = {k: v for k, v in data.items() if v} data = {k: v for k, v in data.items() if v}
result[package] = data result[package] = data
@ -129,13 +161,16 @@ def main():
if recursive > 0: if recursive > 0:
for rdeps in result[package].values(): for rdeps in result[package].values():
for rdep in rdeps: for rdep in rdeps:
build_results( build_results(rdep["Package"], result, fields, component, recursive - 1)
rdep['Package'], result, fields, component, recursive - 1)
result = {} result = {}
build_results( build_results(
options.package, result, fields, options.component, options.package,
options.recursive and options.recursive_depth or 0) result,
fields,
options.component,
options.recursive and options.recursive_depth or 0,
)
if options.list: if options.list:
display_consise(result) display_consise(result)
@ -148,52 +183,59 @@ def display_verbose(package, values):
Logger.info("No reverse dependencies found") Logger.info("No reverse dependencies found")
return return
def log_field(field): def log_package(values, package, arch, dependency, visited, offset=0):
Logger.info(field) line = f"{' ' * offset}* {package}"
Logger.info('=' * len(field))
def log_package(values, package, arch, dependency, offset=0):
line = ' ' * offset + '* %s' % package
if all_archs and set(arch) != all_archs: if all_archs and set(arch) != all_archs:
line += ' [%s]' % ' '.join(sorted(arch)) line += f" [{' '.join(sorted(arch))}]"
if dependency: if dependency:
if len(line) < 30: if len(line) < 30:
line += ' ' * (30 - len(line)) line += " " * (30 - len(line))
line += ' (for %s)' % dependency line += f" (for {dependency})"
Logger.info(line) Logger.info(line)
if package in visited:
return
visited = visited.copy().add(package)
data = values.get(package) data = values.get(package)
if data: if data:
offset = offset + 1 offset = offset + 1
for rdeps in data.values(): for rdeps in data.values():
for rdep in rdeps: for rdep in rdeps:
log_package(values, log_package(
rdep['Package'], values,
rdep.get('Architectures', all_archs), rdep["Package"],
rdep.get('Dependency'), rdep.get("Architectures", all_archs),
offset) rdep.get("Dependency"),
visited,
offset,
)
all_archs = set() all_archs = set()
# This isn't accurate, but we make up for it by displaying what we found # This isn't accurate, but we make up for it by displaying what we found
for data in values.values(): for data in values.values():
for rdeps in data.values(): for rdeps in data.values():
for rdep in rdeps: for rdep in rdeps:
if 'Architectures' in rdep: if "Architectures" in rdep:
all_archs.update(rdep['Architectures']) all_archs.update(rdep["Architectures"])
for field, rdeps in values[package].items(): for field, rdeps in values[package].items():
Logger.info(field) Logger.info("%s", field)
rdeps.sort(key=lambda x: x['Package']) Logger.info("%s", "=" * len(field))
rdeps.sort(key=lambda x: x["Package"])
for rdep in rdeps: for rdep in rdeps:
log_package(values, log_package(
rdep['Package'], values,
rdep.get('Architectures', all_archs), rdep["Package"],
rdep.get('Dependency')) rdep.get("Architectures", all_archs),
rdep.get("Dependency"),
{package},
)
Logger.info("") Logger.info("")
if all_archs: if all_archs:
Logger.info("Packages without architectures listed are " Logger.info(
"reverse-dependencies in: %s" "Packages without architectures listed are reverse-dependencies in: %s",
% ', '.join(sorted(list(all_archs)))) ", ".join(sorted(list(all_archs))),
)
def display_consise(values): def display_consise(values):
@ -201,10 +243,10 @@ def display_consise(values):
for data in values.values(): for data in values.values():
for rdeps in data.values(): for rdeps in data.values():
for rdep in rdeps: for rdep in rdeps:
result.add(rdep['Package']) result.add(rdep["Package"])
Logger.info('\n'.join(sorted(list(result)))) Logger.info("\n".join(sorted(list(result))))
if __name__ == '__main__': if __name__ == "__main__":
main() main()

19
run-linters Executable file
View File

@ -0,0 +1,19 @@
#!/bin/sh
set -eu
# Copyright 2023, Canonical Ltd.
# SPDX-License-Identifier: GPL-3.0
PYTHON_SCRIPTS=$(grep -l -r '^#! */usr/bin/python3$' .)
echo "Running black..."
black --check --diff . $PYTHON_SCRIPTS
echo "Running isort..."
isort --check-only --diff .
echo "Running flake8..."
flake8 --max-line-length=99 --ignore=E203,W503 . $PYTHON_SCRIPTS
echo "Running pylint..."
pylint $(find * -name '*.py') $PYTHON_SCRIPTS

81
running-autopkgtests Executable file
View File

@ -0,0 +1,81 @@
#!/usr/bin/python3
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
# Authors:
# Andy P. Whitcroft
# Christian Ehrhardt
# Chris Peterson <chris.peterson@canonical.com>
#
# Copyright (C) 2024 Canonical Ltd.
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
"""Dumps a list of currently running tests in Autopkgtest"""
__example__ = """
Display first listed test running on amd64 hardware:
$ running-autopkgtests | grep amd64 | head -n1
R 0:01:40 systemd-upstream - focal amd64\
upstream-systemd-ci/systemd-ci - ['CFLAGS=-O0', 'DEB_BUILD_PROFILES=noudeb',\
'TEST_UPSTREAM=1', 'CONFFLAGS_UPSTREAM=--werror -Dslow-tests=true',\
'UPSTREAM_PULL_REQUEST=23153',\
'GITHUB_STATUSES_URL=https://api.github.com/repos/\
systemd/systemd/statuses/cfb0935923dff8050315b5dd22ce8ab06461ff0e']
"""
import sys
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from ubuntutools.running_autopkgtests import get_queued, get_running
def parse_args():
description = (
"Dumps a list of currently running and queued tests in Autopkgtest. "
"Pass --running to only see running tests, or --queued to only see "
"queued tests. Passing both will print both, which is the default behavior. "
)
parser = ArgumentParser(
prog="running-autopkgtests",
description=description,
epilog=f"example: {__example__}",
formatter_class=RawDescriptionHelpFormatter,
)
parser.add_argument(
"-r", "--running", action="store_true", help="Print runnning autopkgtests (default: true)"
)
parser.add_argument(
"-q", "--queued", action="store_true", help="Print queued autopkgtests (default: true)"
)
options = parser.parse_args()
# If neither flag was specified, default to both not neither
if not options.running and not options.queued:
options.running = True
options.queued = True
return options
def main() -> int:
args = parse_args()
if args.running:
print(get_running())
if args.queued:
print(get_queued())
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -14,51 +14,53 @@
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# pylint: disable=invalid-name
# pylint: enable=invalid-name
import argparse
import collections import collections
import gzip import gzip
import json import json
import optparse
import os import os
import time import time
import urllib.request import urllib.request
from ubuntutools.lp.lpapicache import (Distribution, Launchpad,
PackageNotFoundException)
from ubuntutools import getLogger from ubuntutools import getLogger
from ubuntutools.lp.lpapicache import Distribution, Launchpad, PackageNotFoundException
Logger = getLogger() Logger = getLogger()
DATA_URL = 'http://qa.ubuntuwire.org/ubuntu-seeded-packages/seeded.json.gz' DATA_URL = "http://qa.ubuntuwire.org/ubuntu-seeded-packages/seeded.json.gz"
def load_index(url): def load_index(url):
'''Download a new copy of the image contents index, if necessary, """Download a new copy of the image contents index, if necessary,
and read it. and read it.
''' """
cachedir = os.path.expanduser('~/.cache/ubuntu-dev-tools') cachedir = os.path.expanduser("~/.cache/ubuntu-dev-tools")
fn = os.path.join(cachedir, 'seeded.json.gz') seeded = os.path.join(cachedir, "seeded.json.gz")
if (not os.path.isfile(fn) if not os.path.isfile(seeded) or time.time() - os.path.getmtime(seeded) > 60 * 60 * 2:
or time.time() - os.path.getmtime(fn) > 60 * 60 * 2):
if not os.path.isdir(cachedir): if not os.path.isdir(cachedir):
os.makedirs(cachedir) os.makedirs(cachedir)
urllib.request.urlretrieve(url, fn) urllib.request.urlretrieve(url, seeded)
try: try:
with gzip.open(fn, 'r') as f: with gzip.open(seeded, "r") as f:
return json.load(f) return json.load(f)
except Exception as e: except Exception as e: # pylint: disable=broad-except
Logger.error("Unable to parse seed data: %s. " Logger.error(
"Deleting cached data, please try again.", "Unable to parse seed data: %s. Deleting cached data, please try again.", str(e)
str(e)) )
os.unlink(fn) os.unlink(seeded)
return None
def resolve_binaries(sources): def resolve_binaries(sources):
'''Return a dict of source:binaries for all binary packages built by """Return a dict of source:binaries for all binary packages built by
sources sources
''' """
archive = Distribution('ubuntu').getArchive() archive = Distribution("ubuntu").getArchive()
binaries = {} binaries = {}
for source in sources: for source in sources:
try: try:
@ -66,80 +68,84 @@ def resolve_binaries(sources):
except PackageNotFoundException as e: except PackageNotFoundException as e:
Logger.error(str(e)) Logger.error(str(e))
continue continue
binaries[source] = sorted(set(bpph.getPackageName() binaries[source] = sorted(set(bpph.getPackageName() for bpph in spph.getBinaries()))
for bpph in spph.getBinaries()))
return binaries return binaries
def present_on(appearences): def present_on(appearences):
'''Format a list of (flavor, type) tuples into a human-readable string''' """Format a list of (flavor, type) tuples into a human-readable string"""
present = collections.defaultdict(set) present = collections.defaultdict(set)
for flavor, type_ in appearences: for flavor, type_ in appearences:
present[flavor].add(type_) present[flavor].add(type_)
for flavor, types in present.items(): for flavor, types in present.items():
if len(types) > 1: if len(types) > 1:
types.discard('supported') types.discard("supported")
output = [' %s: %s' % (flavor, ', '.join(sorted(types))) output = [f" {flavor}: {', '.join(sorted(types))}" for flavor, types in present.items()]
for flavor, types in present.items()]
output.sort() output.sort()
return '\n'.join(output) return "\n".join(output)
def output_binaries(index, binaries): def output_binaries(index, binaries):
'''Print binaries found in index''' """Print binaries found in index"""
for binary in binaries: for binary in binaries:
if binary in index: if binary in index:
Logger.info("%s is seeded in:" % binary) Logger.info("%s is seeded in:", binary)
Logger.info(present_on(index[binary])) Logger.info(present_on(index[binary]))
else: else:
Logger.info("%s is not seeded (and may not exist)." % binary) Logger.info("%s is not seeded (and may not exist).", binary)
def output_by_source(index, by_source): def output_by_source(index, by_source):
'''Logger.Info(binaries found in index. Grouped by source''' """Logger.Info(binaries found in index. Grouped by source"""
for source, binaries in by_source.items(): for source, binaries in by_source.items():
seen = False seen = False
if not binaries: if not binaries:
Logger.info("Status unknown: No binary packages built by the latest " Logger.info(
"%s.\nTry again using -b and the expected binary packages." "Status unknown: No binary packages built by the latest "
% source) "%s.\nTry again using -b and the expected binary packages.",
source,
)
continue continue
for binary in binaries: for binary in binaries:
if binary in index: if binary in index:
seen = True seen = True
Logger.info("%s (from %s) is seeded in:" % (binary, source)) Logger.info("%s (from %s) is seeded in:", binary, source)
Logger.info(present_on(index[binary])) Logger.info(present_on(index[binary]))
if not seen: if not seen:
Logger.info("%s's binaries are not seeded." % source) Logger.info("%s's binaries are not seeded.", source)
def main(): def main():
'''Query which images the specified packages are on''' """Query which images the specified packages are on"""
parser = optparse.OptionParser('%prog [options] package...') parser = argparse.ArgumentParser(usage="%(prog)s [options] package...")
parser.add_option('-b', '--binary', parser.add_argument(
default=False, action='store_true', "-b",
help="Binary packages are being specified, " "--binary",
"not source packages (fast)") default=False,
parser.add_option('-u', '--data-url', metavar='URL', action="store_true",
default=DATA_URL, help="Binary packages are being specified, not source packages (fast)",
help='URL for the seeded packages index. ' )
'Default: UbuntuWire') parser.add_argument(
options, args = parser.parse_args() "-u",
"--data-url",
if len(args) < 1: metavar="URL",
parser.error("At least one package must be specified") default=DATA_URL,
help="URL for the seeded packages index. Default: UbuntuWire",
)
parser.add_argument("packages", metavar="package", nargs="+", help=argparse.SUPPRESS)
args = parser.parse_args()
# Login anonymously to LP # Login anonymously to LP
Launchpad.login_anonymously() Launchpad.login_anonymously()
index = load_index(options.data_url) index = load_index(args.data_url)
if options.binary: if args.binary:
output_binaries(index, args) output_binaries(index, args.packages)
else: else:
binaries = resolve_binaries(args) binaries = resolve_binaries(args.packages)
output_by_source(index, binaries) output_by_source(index, binaries)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -104,7 +104,7 @@ echo "In order to do packaging work, you'll need a minimal set of packages."
echo "Those, together with other packages which, though optional, have proven" echo "Those, together with other packages which, though optional, have proven"
echo "to be useful, will now be installed." echo "to be useful, will now be installed."
echo echo
sudo apt-get install ubuntu-dev-tools devscripts debhelper cdbs patchutils pbuilder build-essential sudo apt-get install ubuntu-dev-tools devscripts debhelper patchutils pbuilder build-essential
separator2 separator2
echo "Enabling the source repository" echo "Enabling the source repository"

145
setup.py
View File

@ -1,81 +1,100 @@
#!/usr/bin/python3 #!/usr/bin/python3
from setuptools import setup
import glob import glob
import os import pathlib
import re import re
# look/set what version we have from setuptools import setup
changelog = "debian/changelog"
if os.path.exists(changelog):
head = open(changelog, 'r', encoding='utf-8').readline() def get_debian_version() -> str:
"""Look what Debian version we have."""
changelog = pathlib.Path(__file__).parent / "debian" / "changelog"
with changelog.open("r", encoding="utf-8") as changelog_f:
head = changelog_f.readline()
match = re.compile(r".*\((.*)\).*").match(head) match = re.compile(r".*\((.*)\).*").match(head)
if match: if not match:
version = match.group(1) raise ValueError(f"Failed to extract Debian version from '{head}'.")
return match.group(1)
def make_pep440_compliant(version: str) -> str:
"""Convert the version into a PEP440 compliant version."""
public_version_re = re.compile(r"^([0-9][0-9.]*(?:(?:a|b|rc|.post|.dev)[0-9]+)*)\+?")
_, public, local = public_version_re.split(version, maxsplit=1)
if not local:
return version
sanitized_local = re.sub("[+~]+", ".", local).strip(".")
pep440_version = f"{public}+{sanitized_local}"
assert re.match("^[a-zA-Z0-9.]+$", sanitized_local), f"'{pep440_version}' not PEP440 compliant"
return pep440_version
scripts = [ scripts = [
'backportpackage', "backportpackage",
'bitesize', "check-mir",
'check-mir', "check-symbols",
'check-symbols', "dch-repeat",
'dch-repeat', "grab-merge",
'grab-merge', "grep-merges",
'grep-merges', "import-bug-from-debian",
'import-bug-from-debian', "lp-bitesize",
'merge-changelog', "merge-changelog",
'mk-sbuild', "mk-sbuild",
'pbuilder-dist', "pbuilder-dist",
'pbuilder-dist-simple', "pbuilder-dist-simple",
'pull-pkg', "pm-helper",
'pull-debian-debdiff', "pull-pkg",
'pull-debian-source', "pull-debian-debdiff",
'pull-debian-debs', "pull-debian-source",
'pull-debian-ddebs', "pull-debian-debs",
'pull-debian-udebs', "pull-debian-ddebs",
'pull-lp-source', "pull-debian-udebs",
'pull-lp-debs', "pull-lp-source",
'pull-lp-ddebs', "pull-lp-debs",
'pull-lp-udebs', "pull-lp-ddebs",
'pull-ppa-source', "pull-lp-udebs",
'pull-ppa-debs', "pull-ppa-source",
'pull-ppa-ddebs', "pull-ppa-debs",
'pull-ppa-udebs', "pull-ppa-ddebs",
'pull-uca-source', "pull-ppa-udebs",
'pull-uca-debs', "pull-uca-source",
'pull-uca-ddebs', "pull-uca-debs",
'pull-uca-udebs', "pull-uca-ddebs",
'requestbackport', "pull-uca-udebs",
'requestsync', "requestbackport",
'reverse-depends', "requestsync",
'seeded-in-ubuntu', "reverse-depends",
'setup-packaging-environment', "running-autopkgtests",
'sponsor-patch', "seeded-in-ubuntu",
'submittodebian', "setup-packaging-environment",
'syncpackage', "sponsor-patch",
'ubuntu-build', "submittodebian",
'ubuntu-iso', "syncpackage",
'ubuntu-upload-permission', "ubuntu-build",
'update-maintainer', "ubuntu-iso",
"ubuntu-upload-permission",
"update-maintainer",
] ]
data_files = [ data_files = [
('share/bash-completion/completions', glob.glob("bash_completion/*")), ("share/bash-completion/completions", glob.glob("bash_completion/*")),
('share/man/man1', glob.glob("doc/*.1")), ("share/man/man1", glob.glob("doc/*.1")),
('share/man/man5', glob.glob("doc/*.5")), ("share/man/man5", glob.glob("doc/*.5")),
('share/ubuntu-dev-tools', ['enforced-editing-wrapper']), ("share/ubuntu-dev-tools", ["enforced-editing-wrapper"]),
] ]
if __name__ == '__main__': if __name__ == "__main__":
setup( setup(
name='ubuntu-dev-tools', name="ubuntu-dev-tools",
version=version, version=make_pep440_compliant(get_debian_version()),
scripts=scripts, scripts=scripts,
packages=[ packages=[
'ubuntutools', "ubuntutools",
'ubuntutools/lp', "ubuntutools/lp",
'ubuntutools/requestsync', "ubuntutools/requestsync",
'ubuntutools/sponsor_patch', "ubuntutools/sponsor_patch",
'ubuntutools/test', "ubuntutools/test",
], ],
data_files=data_files, data_files=data_files,
test_suite='ubuntutools.test', test_suite="ubuntutools.test",
) )

View File

@ -14,123 +14,153 @@
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
import optparse # pylint: disable=invalid-name
# pylint: enable=invalid-name
import argparse
import logging
import os import os
import shutil import shutil
import sys import sys
import tempfile import tempfile
import logging
from ubuntutools.builder import get_builder
from ubuntutools.config import UDTConfig
from ubuntutools.sponsor_patch.sponsor_patch import sponsor_patch, check_dependencies
from ubuntutools import getLogger from ubuntutools import getLogger
from ubuntutools.builder import get_builder
from ubuntutools.config import UDTConfig
from ubuntutools.sponsor_patch.sponsor_patch import check_dependencies, sponsor_patch
Logger = getLogger() Logger = getLogger()
def parse(script_name): def parse(script_name):
"""Parse the command line parameters.""" """Parse the command line parameters."""
usage = ("%s [options] <bug number>\n" % (script_name) usage = (
+ "One of --upload, --workdir, or --sponsor must be specified.") "%(prog)s [options] <bug number>\n"
epilog = "See %s(1) for more info." % (script_name) "One of --upload, --workdir, or --sponsor must be specified."
parser = optparse.OptionParser(usage=usage, epilog=epilog) )
epilog = f"See {script_name}(1) for more info."
parser = argparse.ArgumentParser(usage=usage, epilog=epilog)
parser.add_option("-b", "--build", dest="build", parser.add_argument(
help="Build the package with the specified builder.", "-b",
action="store_true", default=False) "--build",
parser.add_option("-B", "--builder", dest="builder", default=None, dest="build",
help="Specify the package builder (default pbuilder)") help="Build the package with the specified builder.",
parser.add_option("-e", "--edit", action="store_true",
help="launch sub-shell to allow editing of the patch", )
dest="edit", action="store_true", default=False) parser.add_argument(
parser.add_option("-k", "--key", dest="keyid", default=None, "-B", "--builder", dest="builder", help="Specify the package builder (default pbuilder)"
help="Specify the key ID to be used for signing.") )
parser.add_option("-l", "--lpinstance", dest="lpinstance", default=None, parser.add_argument(
help="Launchpad instance to connect to " "-e",
"(default: production)", "--edit",
metavar="INSTANCE") help="launch sub-shell to allow editing of the patch",
parser.add_option("--no-conf", dest="no_conf", default=False, dest="edit",
help="Don't read config files or environment variables.", action="store_true",
action="store_true") )
parser.add_option("-s", "--sponsor", help="sponsoring; equals -b -u ubuntu", parser.add_argument(
dest="sponsoring", action="store_true", default=False) "-k", "--key", dest="keyid", help="Specify the key ID to be used for signing."
parser.add_option("-u", "--upload", dest="upload", default=None, )
help="Specify an upload destination (default none).") parser.add_argument(
parser.add_option("-U", "--update", dest="update", default=False, "-l",
action="store_true", "--lpinstance",
help="Update the build environment before building.") dest="lpinstance",
parser.add_option("-v", "--verbose", help="print more information", help="Launchpad instance to connect to (default: production)",
dest="verbose", action="store_true", default=False) metavar="INSTANCE",
parser.add_option("-w", "--workdir", dest="workdir", default=None, )
help="Specify a working directory (default is a " parser.add_argument(
"temporary directory, deleted afterwards).") "--no-conf",
dest="no_conf",
help="Don't read config files or environment variables.",
action="store_true",
)
parser.add_argument(
"-s",
"--sponsor",
help="sponsoring; equals -b -u ubuntu",
dest="sponsoring",
action="store_true",
)
parser.add_argument(
"-u", "--upload", dest="upload", help="Specify an upload destination (default none)."
)
parser.add_argument(
"-U",
"--update",
dest="update",
action="store_true",
help="Update the build environment before building.",
)
parser.add_argument(
"-v", "--verbose", help="print more information", dest="verbose", action="store_true"
)
parser.add_argument(
"-w",
"--workdir",
dest="workdir",
help="Specify a working directory (default is a "
"temporary directory, deleted afterwards).",
)
parser.add_argument("bug_number", type=int, help=argparse.SUPPRESS)
(options, args) = parser.parse_args() args = parser.parse_args()
if options.verbose: if args.verbose:
Logger.setLevel(logging.DEBUG) Logger.setLevel(logging.DEBUG)
check_dependencies() check_dependencies()
if len(args) == 0: config = UDTConfig(args.no_conf)
Logger.error("No bug number specified.") if args.builder is None:
sys.exit(1) args.builder = config.get_value("BUILDER")
elif len(args) > 1: if args.lpinstance is None:
Logger.error("Multiple bug numbers specified: %s" % (", ".join(args))) args.lpinstance = config.get_value("LPINSTANCE")
sys.exit(1) if not args.update:
args.update = config.get_value("UPDATE_BUILDER", boolean=True)
if args.workdir is None:
args.workdir = config.get_value("WORKDIR")
if args.keyid is None:
args.keyid = config.get_value("KEYID")
bug_number = args[0] if args.sponsoring:
if bug_number.isdigit(): args.build = True
bug_number = int(bug_number) args.upload = "ubuntu"
else:
Logger.error("Invalid bug number specified: %s" % (bug_number))
sys.exit(1)
config = UDTConfig(options.no_conf) return args
if options.builder is None:
options.builder = config.get_value("BUILDER")
if options.lpinstance is None:
options.lpinstance = config.get_value("LPINSTANCE")
if not options.update:
options.update = config.get_value("UPDATE_BUILDER", boolean=True)
if options.workdir is None:
options.workdir = config.get_value("WORKDIR")
if options.keyid is None:
options.keyid = config.get_value("KEYID")
if options.sponsoring:
options.build = True
options.upload = "ubuntu"
return (options, bug_number)
def main(): def main():
script_name = os.path.basename(sys.argv[0]) script_name = os.path.basename(sys.argv[0])
(options, bug_number) = parse(script_name) args = parse(script_name)
builder = get_builder(options.builder) builder = get_builder(args.builder)
if not builder: if not builder:
sys.exit(1) sys.exit(1)
if not options.upload and not options.workdir: if not args.upload and not args.workdir:
Logger.error("Please specify either a working directory or an upload " Logger.error("Please specify either a working directory or an upload target!")
"target!")
sys.exit(1) sys.exit(1)
if options.workdir is None: if args.workdir is None:
workdir = tempfile.mkdtemp(prefix=script_name+"-") workdir = tempfile.mkdtemp(prefix=script_name + "-")
else: else:
workdir = options.workdir workdir = args.workdir
try: try:
sponsor_patch(bug_number, options.build, builder, options.edit, sponsor_patch(
options.keyid, options.lpinstance, options.update, args.bug_number,
options.upload, workdir) args.build,
builder,
args.edit,
args.keyid,
args.lpinstance,
args.update,
args.upload,
workdir,
)
except KeyboardInterrupt: except KeyboardInterrupt:
Logger.error("User abort.") Logger.error("User abort.")
sys.exit(2) sys.exit(2)
finally: finally:
if options.workdir is None: if args.workdir is None:
shutil.rmtree(workdir) shutil.rmtree(workdir)

View File

@ -22,32 +22,36 @@
# #
# ################################################################## # ##################################################################
import optparse """Submit the Ubuntu changes in a package to Debian.
Run inside an unpacked Ubuntu source package.
"""
import argparse
import os import os
import re import re
import shutil import shutil
import sys import sys
from subprocess import DEVNULL, PIPE, Popen, call, check_call, run
from subprocess import call, check_call, run, Popen, PIPE, DEVNULL
from tempfile import mkdtemp from tempfile import mkdtemp
from debian.changelog import Changelog from debian.changelog import Changelog
from distro_info import UbuntuDistroInfo, DistroDataOutdated from distro_info import DistroDataOutdated, UbuntuDistroInfo
from ubuntutools.config import ubu_email
from ubuntutools.question import YesNoQuestion, EditFile
from ubuntutools.update_maintainer import update_maintainer, restore_maintainer
from ubuntutools import getLogger from ubuntutools import getLogger
from ubuntutools.config import ubu_email
from ubuntutools.question import EditFile, YesNoQuestion
from ubuntutools.update_maintainer import restore_maintainer, update_maintainer
Logger = getLogger() Logger = getLogger()
def get_most_recent_debian_version(changelog): def get_most_recent_debian_version(changelog):
for block in changelog: for block in changelog:
version = block.version.full_version version = block.version.full_version
if not re.search('(ubuntu|build)', version): if not re.search("(ubuntu|build)", version):
return version return version
return None
def get_bug_body(changelog): def get_bug_body(changelog):
@ -65,19 +69,20 @@ In Ubuntu, the attached patch was applied to achieve the following:
%s %s
Thanks for considering the patch. Thanks for considering the patch.
""" % ("\n".join([a for a in entry.changes()])) """ % (
"\n".join(entry.changes())
)
return msg return msg
def build_source_package(): def build_source_package():
if os.path.isdir('.bzr'): if os.path.isdir(".bzr"):
cmd = ['bzr', 'bd', '--builder=dpkg-buildpackage', '-S', cmd = ["bzr", "bd", "--builder=dpkg-buildpackage", "-S", "--", "-uc", "-us", "-nc"]
'--', '-uc', '-us', '-nc']
else: else:
cmd = ['dpkg-buildpackage', '-S', '-uc', '-us', '-nc'] cmd = ["dpkg-buildpackage", "-S", "-uc", "-us", "-nc"]
env = os.environ.copy() env = os.environ.copy()
# Unset DEBEMAIL in case there's an @ubuntu.com e-mail address # Unset DEBEMAIL in case there's an @ubuntu.com e-mail address
env.pop('DEBEMAIL', None) env.pop("DEBEMAIL", None)
check_call(cmd, env=env) check_call(cmd, env=env)
@ -88,30 +93,35 @@ def gen_debdiff(tmpdir, changelog):
newver = next(changelog_it).version newver = next(changelog_it).version
oldver = next(changelog_it).version oldver = next(changelog_it).version
debdiff = os.path.join(tmpdir, '%s_%s.debdiff' % (pkg, newver)) debdiff = os.path.join(tmpdir, f"{pkg}_{newver}.debdiff")
diff_cmd = ['bzr', 'diff', '-r', 'tag:' + str(oldver)] diff_cmd = ["bzr", "diff", "-r", "tag:" + str(oldver)]
if call(diff_cmd, stdout=DEVNULL, stderr=DEVNULL) == 1: if call(diff_cmd, stdout=DEVNULL, stderr=DEVNULL) == 1:
Logger.info("Extracting bzr diff between %s and %s" % (oldver, newver)) Logger.info("Extracting bzr diff between %s and %s", oldver, newver)
else: else:
if oldver.epoch is not None: if oldver.epoch is not None:
oldver = str(oldver)[str(oldver).index(":") + 1:] oldver = str(oldver)[str(oldver).index(":") + 1 :]
if newver.epoch is not None: if newver.epoch is not None:
newver = str(newver)[str(newver).index(":") + 1:] newver = str(newver)[str(newver).index(":") + 1 :]
olddsc = '../%s_%s.dsc' % (pkg, oldver) olddsc = f"../{pkg}_{oldver}.dsc"
newdsc = '../%s_%s.dsc' % (pkg, newver) newdsc = f"../{pkg}_{newver}.dsc"
check_file(olddsc) check_file(olddsc)
check_file(newdsc) check_file(newdsc)
Logger.info("Generating debdiff between %s and %s" % (oldver, newver)) Logger.info("Generating debdiff between %s and %s", oldver, newver)
diff_cmd = ['debdiff', olddsc, newdsc] diff_cmd = ["debdiff", olddsc, newdsc]
with Popen(diff_cmd, stdout=PIPE, encoding='utf-8') as diff: with Popen(diff_cmd, stdout=PIPE, encoding="utf-8") as diff:
with open(debdiff, 'w', encoding='utf-8') as debdiff_f: with open(debdiff, "w", encoding="utf-8") as debdiff_f:
run(['filterdiff', '-x', '*changelog*'], run(
stdin=diff.stdout, stdout=debdiff_f, encoding='utf-8') ["filterdiff", "-x", "*changelog*"],
check=False,
stdin=diff.stdout,
stdout=debdiff_f,
encoding="utf-8",
)
return debdiff return debdiff
@ -119,11 +129,10 @@ def gen_debdiff(tmpdir, changelog):
def check_file(fname, critical=True): def check_file(fname, critical=True):
if os.path.exists(fname): if os.path.exists(fname):
return fname return fname
else: if not critical:
if not critical: return False
return False Logger.info("Couldn't find «%s».\n", fname)
Logger.info("Couldn't find «%s».\n" % fname) sys.exit(1)
sys.exit(1)
def submit_bugreport(body, debdiff, deb_version, changelog): def submit_bugreport(body, debdiff, deb_version, changelog):
@ -131,76 +140,84 @@ def submit_bugreport(body, debdiff, deb_version, changelog):
devel = UbuntuDistroInfo().devel() devel = UbuntuDistroInfo().devel()
except DistroDataOutdated as e: except DistroDataOutdated as e:
Logger.info(str(e)) Logger.info(str(e))
devel = '' devel = ""
if os.path.dirname(sys.argv[0]).startswith('/usr/bin'): if os.path.dirname(sys.argv[0]).startswith("/usr/bin"):
editor_path = '/usr/share/ubuntu-dev-tools' editor_path = "/usr/share/ubuntu-dev-tools"
else: else:
editor_path = os.path.dirname(sys.argv[0]) editor_path = os.path.dirname(sys.argv[0])
env = dict(os.environ.items()) env = dict(os.environ.items())
if 'EDITOR' in env: if "EDITOR" in env:
env['UDT_EDIT_WRAPPER_EDITOR'] = env['EDITOR'] env["UDT_EDIT_WRAPPER_EDITOR"] = env["EDITOR"]
if 'VISUAL' in env: if "VISUAL" in env:
env['UDT_EDIT_WRAPPER_VISUAL'] = env['VISUAL'] env["UDT_EDIT_WRAPPER_VISUAL"] = env["VISUAL"]
env['EDITOR'] = os.path.join(editor_path, 'enforced-editing-wrapper') env["EDITOR"] = os.path.join(editor_path, "enforced-editing-wrapper")
env['VISUAL'] = os.path.join(editor_path, 'enforced-editing-wrapper') env["VISUAL"] = os.path.join(editor_path, "enforced-editing-wrapper")
env['UDT_EDIT_WRAPPER_TEMPLATE_RE'] = ( env["UDT_EDIT_WRAPPER_TEMPLATE_RE"] = ".*REPLACE THIS WITH ACTUAL INFORMATION.*"
'.*REPLACE THIS WITH ACTUAL INFORMATION.*') env["UDT_EDIT_WRAPPER_FILE_DESCRIPTION"] = "bug report"
env['UDT_EDIT_WRAPPER_FILE_DESCRIPTION'] = 'bug report'
# In external mua mode, attachments are lost (Reportbug bug: #679907) # In external mua mode, attachments are lost (Reportbug bug: #679907)
internal_mua = True internal_mua = True
for cfgfile in ('/etc/reportbug.conf', '~/.reportbugrc'): for cfgfile in ("/etc/reportbug.conf", "~/.reportbugrc"):
cfgfile = os.path.expanduser(cfgfile) cfgfile = os.path.expanduser(cfgfile)
if not os.path.exists(cfgfile): if not os.path.exists(cfgfile):
continue continue
with open(cfgfile, 'r') as f: with open(cfgfile, "r", encoding="utf-8") as f:
for line in f: for line in f:
line = line.strip() line = line.strip()
if line in ('gnus', 'mutt', 'nmh') or line.startswith('mua '): if line in ("gnus", "mutt", "nmh") or line.startswith("mua "):
internal_mua = False internal_mua = False
break break
cmd = ('reportbug', cmd = (
'--no-check-available', "reportbug",
'--no-check-installed', "--no-check-available",
'--pseudo-header', 'User: ubuntu-devel@lists.ubuntu.com', "--no-check-installed",
'--pseudo-header', 'Usertags: origin-ubuntu %s ubuntu-patch' "--pseudo-header",
% devel, "User: ubuntu-devel@lists.ubuntu.com",
'--tag', 'patch', "--pseudo-header",
'--bts', 'debian', f"Usertags: origin-ubuntu {devel} ubuntu-patch",
'--include', body, "--tag",
'--attach' if internal_mua else '--include', debdiff, "patch",
'--package-version', deb_version, "--bts",
changelog.package) "debian",
"--include",
body,
"--attach" if internal_mua else "--include",
debdiff,
"--package-version",
deb_version,
changelog.package,
)
check_call(cmd, env=env) check_call(cmd, env=env)
def check_reportbug_config(): def check_reportbug_config():
fn = os.path.expanduser('~/.reportbugrc') reportbugrc_filename = os.path.expanduser("~/.reportbugrc")
if os.path.exists(fn): if os.path.exists(reportbugrc_filename):
return return
email = ubu_email()[1] email = ubu_email()[1]
reportbugrc = """# Reportbug configuration generated by submittodebian(1) reportbugrc = f"""# Reportbug configuration generated by submittodebian(1)
# See reportbug.conf(5) for the configuration file format. # See reportbug.conf(5) for the configuration file format.
# Use Debian's reportbug SMTP Server: # Use Debian's reportbug SMTP Server:
# Note: it's limited to 5 connections per hour, and cannot CC you at submission # Note: it's limited to 5 connections per hour, and cannot CC you at submission
# time. See /usr/share/doc/reportbug/README.Users.gz for more details. # time. See /usr/share/doc/reportbug/README.Users.gz for more details.
smtphost reportbug.debian.org:587 smtphost reportbug.debian.org:587
header "X-Debbugs-CC: %s" header "X-Debbugs-CC: {email}"
no-cc no-cc
# Use GMail's SMTP Server: # Use GMail's SMTP Server:
#smtphost smtp.googlemail.com:587 #smtphost smtp.googlemail.com:587
#smtpuser "<your address>@gmail.com" #smtpuser "<your address>@gmail.com"
#smtptls #smtptls
""" % email """
with open(fn, 'w') as f: with open(reportbugrc_filename, "w", encoding="utf-8") as f:
f.write(reportbugrc) f.write(reportbugrc)
Logger.info("""\ Logger.info(
"""\
You have not configured reportbug. Assuming this is the first time you have You have not configured reportbug. Assuming this is the first time you have
used it. Writing a ~/.reportbugrc that will use Debian's mail server, and CC used it. Writing a ~/.reportbugrc that will use Debian's mail server, and CC
the bug to you at <%s> the bug to you at <%s>
@ -211,40 +228,43 @@ the bug to you at <%s>
If this is not correct, please exit now and edit ~/.reportbugrc or run If this is not correct, please exit now and edit ~/.reportbugrc or run
reportbug --configure for its configuration wizard. reportbug --configure for its configuration wizard.
""" % (email, reportbugrc.strip())) """,
email,
reportbugrc.strip(),
)
if YesNoQuestion().ask("Continue submitting this bug", "yes") == "no": if YesNoQuestion().ask("Continue submitting this bug", "yes") == "no":
sys.exit(1) sys.exit(1)
def main(): def main():
description = 'Submit the Ubuntu changes in a package to Debian. ' + \ parser = argparse.ArgumentParser(description=__doc__)
'Run inside an unpacked Ubuntu source package.'
parser = optparse.OptionParser(description=description)
parser.parse_args() parser.parse_args()
if not os.path.exists('/usr/bin/reportbug'): if not os.path.exists("/usr/bin/reportbug"):
Logger.error("This utility requires the «reportbug» package, which isn't " Logger.error(
"currently installed.") "This utility requires the «reportbug» package, which isn't currently installed."
)
sys.exit(1) sys.exit(1)
check_reportbug_config() check_reportbug_config()
changelog_file = (check_file('debian/changelog', critical=False) or changelog_file = check_file("debian/changelog", critical=False) or check_file(
check_file('../debian/changelog')) "../debian/changelog"
with open(changelog_file) as f: )
with open(changelog_file, encoding="utf-8") as f:
changelog = Changelog(f.read()) changelog = Changelog(f.read())
deb_version = get_most_recent_debian_version(changelog) deb_version = get_most_recent_debian_version(changelog)
bug_body = get_bug_body(changelog) bug_body = get_bug_body(changelog)
tmpdir = mkdtemp() tmpdir = mkdtemp()
body = os.path.join(tmpdir, 'bug_body') body = os.path.join(tmpdir, "bug_body")
with open(body, 'wb') as f: with open(body, "wb") as f:
f.write(bug_body.encode('utf-8')) f.write(bug_body.encode("utf-8"))
restore_maintainer('debian') restore_maintainer("debian")
build_source_package() build_source_package()
update_maintainer('debian') update_maintainer("debian")
debdiff = gen_debdiff(tmpdir, changelog) debdiff = gen_debdiff(tmpdir, changelog)
@ -252,7 +272,7 @@ def main():
# reverted in the most recent build # reverted in the most recent build
build_source_package() build_source_package()
EditFile(debdiff, 'debdiff').edit(optional=True) EditFile(debdiff, "debdiff").edit(optional=True)
submit_bugreport(body, debdiff, deb_version, changelog) submit_bugreport(body, debdiff, deb_version, changelog)
os.unlink(body) os.unlink(body)
@ -260,5 +280,5 @@ def main():
shutil.rmtree(tmpdir) shutil.rmtree(tmpdir)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

File diff suppressed because it is too large Load Diff

View File

@ -2,16 +2,16 @@
# #
# ubuntu-build - command line interface for Launchpad buildd operations. # ubuntu-build - command line interface for Launchpad buildd operations.
# #
# Copyright (C) 2007 Canonical Ltd. # Copyright (C) 2007-2024 Canonical Ltd.
# Authors: # Authors:
# - Martin Pitt <martin.pitt@canonical.com> # - Martin Pitt <martin.pitt@canonical.com>
# - Jonathan Davies <jpds@ubuntu.com> # - Jonathan Davies <jpds@ubuntu.com>
# - Michael Bienia <geser@ubuntu.com> # - Michael Bienia <geser@ubuntu.com>
# - Steve Langasek <steve.langasek@canonical.com>
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or # the Free Software Foundation, version 3 of the License.
# (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful, # This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
@ -22,106 +22,181 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
# Our modules to import. # pylint: disable=invalid-name
# pylint: enable=invalid-name
import argparse
import sys import sys
from optparse import OptionGroup
from optparse import OptionParser import lazr.restfulclient.errors
from ubuntutools.lp.udtexceptions import (SeriesNotFoundException, from launchpadlib.launchpad import Launchpad
PackageNotFoundException,
PocketDoesNotExistError,)
from ubuntutools.lp.lpapicache import Distribution, PersonTeam
from ubuntutools.misc import split_release_pocket
from ubuntutools import getLogger from ubuntutools import getLogger
from ubuntutools.lp.udtexceptions import PocketDoesNotExistError
from ubuntutools.misc import split_release_pocket
Logger = getLogger() Logger = getLogger()
def get_build_states(pkg, archs):
res = []
for build in pkg.getBuilds():
if build.arch_tag in archs:
res.append(f" {build.arch_tag}: {build.buildstate}")
msg = "\n".join(res)
return f"Build state(s) for '{pkg.source_package_name}':\n{msg}"
def rescore_builds(pkg, archs, score):
res = []
for build in pkg.getBuilds():
arch = build.arch_tag
if arch in archs:
if not build.can_be_rescored:
continue
try:
build.rescore(score=score)
res.append(f" {arch}: done")
except lazr.restfulclient.errors.Unauthorized:
Logger.error(
"You don't have the permissions to rescore builds."
" Ignoring your rescore request."
)
return None
except lazr.restfulclient.errors.BadRequest:
Logger.info("Cannot rescore build of %s on %s.", build.source_package_name, arch)
res.append(f" {arch}: failed")
msg = "\n".join(res)
return f"Rescoring builds of '{pkg.source_package_name}' to {score}:\n{msg}"
def retry_builds(pkg, archs):
res = []
for build in pkg.getBuilds():
arch = build.arch_tag
if arch in archs:
try:
build.retry()
res.append(f" {arch}: done")
except lazr.restfulclient.errors.BadRequest:
res.append(f" {arch}: failed")
msg = "\n".join(res)
return f"Retrying builds of '{pkg.source_package_name}':\n{msg}"
def main(): def main():
# Usage. # Usage.
usage = "%prog <srcpackage> <release> <operation>\n\n" usage = "%(prog)s <srcpackage> <release> <operation>\n\n"
usage += "Where operation may be one of: rescore, retry, or status.\n" usage += "Where operation may be one of: rescore, retry, or status.\n"
usage += "Only Launchpad Buildd Admins may rescore package builds." usage += "Only Launchpad Buildd Admins may rescore package builds."
# Valid architectures. # Valid architectures.
valid_archs = set([ valid_archs = set(
"armel", "armhf", "arm64", "amd64", "hppa", "i386", "ia64", ["armhf", "arm64", "amd64", "i386", "powerpc", "ppc64el", "riscv64", "s390x"]
"lpia", "powerpc", "ppc64el", "riscv64", "s390x", "sparc", )
])
# Prepare our option parser. # Prepare our option parser.
opt_parser = OptionParser(usage) parser = argparse.ArgumentParser(usage=usage)
# Retry options parser.add_argument(
retry_rescore_options = OptionGroup(opt_parser, "Retry and rescore options", "-a",
"These options may only be used with " "--arch",
"the 'retry' and 'rescore' operations.") action="append",
retry_rescore_options.add_option("-a", "--arch", type="string", dest="architecture",
action="append", dest="architecture", help=f"Rebuild or rescore a specific architecture. Valid architectures "
help="Rebuild or rescore a specific " f"include: {', '.join(valid_archs)}.",
"architecture. Valid architectures " )
"include: %s." %
", ".join(valid_archs)) parser.add_argument("-A", "--archive", help="operate on ARCHIVE", default="ubuntu")
# Batch processing options # Batch processing options
batch_options = OptionGroup(opt_parser, "Batch processing", batch_options = parser.add_argument_group(
"These options and parameter ordering is only " "Batch processing",
"available in --batch mode.\nUsage: " "These options and parameter ordering is only "
"ubuntu-build --batch [options] <package>...") "available in --batch mode.\nUsage: "
batch_options.add_option('--batch', "ubuntu-build --batch [options] <package>...",
action='store_true', dest='batch', default=False, )
help='Enable batch mode') batch_options.add_argument(
batch_options.add_option('--series', "--batch", action="store_true", dest="batch", help="Enable batch mode"
action='store', dest='series', type='string', )
help='Selects the Ubuntu series to operate on ' batch_options.add_argument(
'(default: current development series)') "--series",
batch_options.add_option('--retry', action="store",
action='store_true', dest='retry', default=False, dest="series",
help='Retry builds (give-back).') help="Selects the Ubuntu series to operate on (default: current development series)",
batch_options.add_option('--rescore', )
action='store', dest='priority', type='int', batch_options.add_argument(
help='Rescore builds to <priority>.') "--retry", action="store_true", dest="retry", help="Retry builds (give-back)."
batch_options.add_option('--arch2', action='append', dest='architecture', )
type='string', batch_options.add_argument(
help="Affect only 'architecture' (can be used " "--rescore",
"several times). Valid architectures are: %s." action="store",
% ', '.join(valid_archs)) dest="priority",
type=int,
help="Rescore builds to <priority>.",
)
batch_options.add_argument(
"--state",
action="store",
dest="state",
help="Act on builds that are in the specified state",
)
# Add the retry options to the main group. parser.add_argument("packages", metavar="package", nargs="*", help=argparse.SUPPRESS)
opt_parser.add_option_group(retry_rescore_options)
# Add the batch mode to the main group.
opt_parser.add_option_group(batch_options)
# Parse our options. # Parse our options.
(options, args) = opt_parser.parse_args() args = parser.parse_args()
if not len(args): launchpad = Launchpad.login_with("ubuntu-dev-tools", "production", version="devel")
opt_parser.print_help() ubuntu = launchpad.distributions["ubuntu"]
sys.exit(1)
if not options.batch: if args.batch:
release = args.series
if not release:
# ppas don't have a proposed pocket so just use the release pocket;
# but for the main archive we default to -proposed
release = ubuntu.getDevelopmentSeries()[0].name
if args.archive == "ubuntu":
release = f"{release}-proposed"
try:
(release, pocket) = split_release_pocket(release)
except PocketDoesNotExistError as error:
Logger.error(error)
sys.exit(1)
else:
# Check we have the correct number of arguments. # Check we have the correct number of arguments.
if len(args) < 3: if len(args.packages) < 3:
opt_parser.error("Incorrect number of arguments.") parser.error("Incorrect number of arguments.")
try: try:
package = str(args[0]).lower() package = str(args.packages[0]).lower()
release = str(args[1]).lower() release = str(args.packages[1]).lower()
op = str(args[2]).lower() operation = str(args.packages[2]).lower()
except IndexError: except IndexError:
opt_parser.print_help() parser.print_help()
sys.exit(1) sys.exit(1)
archive = launchpad.archives.getByReference(reference=args.archive)
try:
distroseries = ubuntu.getSeries(name_or_version=release)
except lazr.restfulclient.errors.NotFound as error:
Logger.error(error)
sys.exit(1)
if not args.batch:
# Check our operation. # Check our operation.
if op not in ("rescore", "retry", "status"): if operation not in ("rescore", "retry", "status"):
Logger.error("Invalid operation: %s." % op) Logger.error("Invalid operation: %s.", operation)
sys.exit(1) sys.exit(1)
# If the user has specified an architecture to build, we only wish to # If the user has specified an architecture to build, we only wish to
# rebuild it and nothing else. # rebuild it and nothing else.
if options.architecture: if args.architecture:
if options.architecture[0] not in valid_archs: if args.architecture[0] not in valid_archs:
Logger.error("Invalid architecture specified: %s." Logger.error("Invalid architecture specified: %s.", args.architecture[0])
% options.architecture[0])
sys.exit(1) sys.exit(1)
else: else:
one_arch = True one_arch = True
@ -135,148 +210,239 @@ def main():
Logger.error(error) Logger.error(error)
sys.exit(1) sys.exit(1)
# Get the ubuntu archive
try:
ubuntu_archive = Distribution('ubuntu').getArchive()
# Will fail here if we have no credentials, bail out
except IOError:
sys.exit(1)
# Get list of published sources for package in question. # Get list of published sources for package in question.
try: try:
sources = ubuntu_archive.getSourcePackage(package, release, pocket) sources = archive.getPublishedSources(
distroseries = Distribution('ubuntu').getSeries(release) distro_series=distroseries,
except (SeriesNotFoundException, PackageNotFoundException) as error: exact_match=True,
Logger.error(error) pocket=pocket,
source_name=package,
status="Published",
)[0]
except IndexError:
Logger.error("No publication found for package %s", package)
sys.exit(1) sys.exit(1)
# Get list of builds for that package. # Get list of builds for that package.
builds = sources.getBuilds() builds = sources.getBuilds()
# Find out the version and component in given release. # Find out the version and component in given release.
version = sources.getVersion() version = sources.source_package_version
component = sources.getComponent() component = sources.component_name
# Operations that are remaining may only be done by Ubuntu developers # Operations that are remaining may only be done by Ubuntu developers
# (retry) or buildd admins (rescore). Check if the proper permissions # (retry) or buildd admins (rescore). Check if the proper permissions
# are in place. # are in place.
me = PersonTeam.me if operation == "retry":
if op == "rescore": necessary_privs = archive.checkUpload(
necessary_privs = me.isLpTeamMember('launchpad-buildd-admins') component=sources.getComponent(),
if op == "retry": distroseries=distroseries,
necessary_privs = me.canUploadPackage(ubuntu_archive, distroseries, person=launchpad.me,
sources.getPackageName(), pocket=pocket,
sources.getComponent(), sourcepackagename=sources.getPackageName(),
pocket=pocket) )
if not necessary_privs:
if op in ('rescore', 'retry') and not necessary_privs: Logger.error(
Logger.error("You cannot perform the %s operation on a %s " "You cannot perform the %s operation on a %s package as you"
"package as you do not have the permissions " " do not have the permissions to do this action.",
"to do this action." % (op, component)) operation,
sys.exit(1) component,
)
sys.exit(1)
# Output details. # Output details.
Logger.info("The source version for '%s' in %s (%s) is at %s." % Logger.info(
(package, release.capitalize(), component, version)) "The source version for '%s' in %s (%s) is at %s.",
package,
release.capitalize(),
component,
version,
)
Logger.info("Current build status for this package:") Logger.info("Current build status for this package:")
# Output list of arches for package and their status. # Output list of arches for package and their status.
done = False done = False
for build in builds: for build in builds:
if one_arch and build.arch_tag != options.architecture[0]: if one_arch and build.arch_tag != args.architecture[0]:
# Skip this architecture. # Skip this architecture.
continue continue
done = True done = True
Logger.info("%s: %s." % (build.arch_tag, build.buildstate)) Logger.info("%s: %s.", build.arch_tag, build.buildstate)
if op == 'rescore': if operation == "rescore":
if build.can_be_rescored: if build.can_be_rescored:
# FIXME: make priority an option # FIXME: make priority an option
priority = 5000 priority = 5000
Logger.info('Rescoring build %s to %d...' % (build.arch_tag, priority)) Logger.info("Rescoring build %s to %d...", build.arch_tag, priority)
build.rescore(score=priority) try:
build.rescore(score=priority)
except lazr.restfulclient.errors.Unauthorized:
Logger.error(
"You don't have the permissions to rescore builds."
" Ignoring your rescore request."
)
break
else: else:
Logger.info('Cannot rescore build on %s.' % build.arch_tag) Logger.info("Cannot rescore build on %s.", build.arch_tag)
if op == 'retry': if operation == "retry":
if build.can_be_retried: if build.can_be_retried:
Logger.info('Retrying build on %s...' % build.arch_tag) Logger.info("Retrying build on %s...", build.arch_tag)
build.retry() build.retry()
else: else:
Logger.info('Cannot retry build on %s.' % build.arch_tag) Logger.info("Cannot retry build on %s.", build.arch_tag)
# We are done # We are done
if done: if done:
sys.exit(0) sys.exit(0)
Logger.info("No builds for '%s' found in the %s release" % (package, release.capitalize())) Logger.info("No builds for '%s' found in the %s release", package, release.capitalize())
Logger.info("It may have been built in a former release.") Logger.info("It may have been built in a former release.")
sys.exit(0) sys.exit(0)
# Batch mode # Batch mode
if not options.architecture: if not args.architecture:
# no specific architectures specified, assume all valid ones # no specific architectures specified, assume all valid ones
archs = valid_archs archs = valid_archs
else: else:
archs = set(options.architecture) archs = set(args.architecture)
# filter out duplicate and invalid architectures # filter out duplicate and invalid architectures
archs.intersection_update(valid_archs) archs.intersection_update(valid_archs)
release = options.series if not args.packages:
if not release: retry_count = 0
release = (Distribution('ubuntu').getDevelopmentSeries().name can_rescore = True
+ '-proposed')
try:
(release, pocket) = split_release_pocket(release)
except PocketDoesNotExistError as error:
Logger.error(error)
sys.exit(1)
ubuntu_archive = Distribution('ubuntu').getArchive() if not args.state:
try: if args.retry:
distroseries = Distribution('ubuntu').getSeries(release) args.state = "Failed to build"
except SeriesNotFoundException as error: elif args.priority:
Logger.error(error) args.state = "Needs building"
sys.exit(1) # there is no equivalent to series.getBuildRecords() for a ppa.
me = PersonTeam.me # however, we don't want to have to traverse all build records for
# all series when working on the main archive, so we use
# series.getBuildRecords() for ubuntu and handle ppas separately
series = ubuntu.getSeries(name_or_version=release)
if args.archive == "ubuntu":
builds = series.getBuildRecords(build_state=args.state, pocket=pocket)
else:
builds = []
for build in archive.getBuildRecords(build_state=args.state, pocket=pocket):
if not build.current_source_publication:
continue
if build.current_source_publication.distro_series == series:
builds.append(build)
for build in builds:
if build.arch_tag not in archs:
continue
if not build.current_source_publication:
continue
# fixme: refactor
# Check permissions (part 2): check upload permissions for the
# source package
can_retry = args.retry and archive.checkUpload(
component=build.current_source_publication.component_name,
distroseries=series,
person=launchpad.me,
pocket=pocket,
sourcepackagename=build.source_package_name,
)
if args.retry and not can_retry:
Logger.error(
"You don't have the permissions to retry the build of '%s', skipping.",
build.source_package_name,
)
continue
Logger.info(
"The source version for '%s' in '%s' (%s) is: %s",
build.source_package_name,
release,
pocket,
build.source_package_version,
)
# Check permisions (part 1): Rescoring can only be done by buildd admins if args.retry and build.can_be_retried:
can_rescore = ((options.priority Logger.info(
and me.isLpTeamMember('launchpad-buildd-admins')) "Retrying build of %s on %s...", build.source_package_name, build.arch_tag
or False) )
if options.priority and not can_rescore: try:
Logger.error("You don't have the permissions to rescore " build.retry()
"builds. Ignoring your rescore request.") retry_count += 1
except lazr.restfulclient.errors.BadRequest:
Logger.info(
"Failed to retry build of %s on %s",
build.source_package_name,
build.arch_tag,
)
for pkg in args: if args.priority and can_rescore:
if build.can_be_rescored:
try:
build.rescore(score=args.priority)
except lazr.restfulclient.errors.Unauthorized:
Logger.error(
"You don't have the permissions to rescore builds."
" Ignoring your rescore request."
)
can_rescore = False
except lazr.restfulclient.errors.BadRequest:
Logger.info(
"Cannot rescore build of %s on %s.",
build.source_package_name,
build.arch_tag,
)
Logger.info("")
if args.retry:
Logger.info("%d package builds retried", retry_count)
sys.exit(0)
for pkg in args.packages:
try: try:
pkg = ubuntu_archive.getSourcePackage(pkg, release, pocket) pkg = archive.getPublishedSources(
except PackageNotFoundException as error: distro_series=distroseries,
Logger.error(error) exact_match=True,
pocket=pocket,
source_name=pkg,
status="Published",
)[0]
except IndexError:
Logger.error("No publication found for package %s", pkg)
continue continue
# Check permissions (part 2): check upload permissions for the source # Check permissions (part 2): check upload permissions for the source
# package # package
can_retry = options.retry and me.canUploadPackage(ubuntu_archive, can_retry = args.retry and archive.checkUpload(
distroseries, component=pkg.component_name,
pkg.getPackageName(), distroseries=distroseries,
pkg.getComponent()) person=launchpad.me,
if options.retry and not can_retry: pocket=pocket,
Logger.error("You don't have the permissions to retry the " sourcepackagename=pkg.source_package_name,
"build of '%s'. Ignoring your request." )
% pkg.getPackageName()) if args.retry and not can_retry:
Logger.error(
"You don't have the permissions to retry the "
"build of '%s'. Ignoring your request.",
pkg.source_package_name,
)
Logger.info("The source version for '%s' in '%s' (%s) is: %s" % Logger.info(
(pkg.getPackageName(), release, pocket, pkg.getVersion())) "The source version for '%s' in '%s' (%s) is: %s",
pkg.source_package_name,
release,
pocket,
pkg.source_package_version,
)
Logger.info(pkg.getBuildStates(archs)) Logger.info(get_build_states(pkg, archs))
if can_retry: if can_retry:
Logger.info(pkg.retryBuilds(archs)) Logger.info(retry_builds(pkg, archs))
if options.priority and can_rescore: if args.priority:
Logger.info(pkg.rescoreBuilds(archs, options.priority)) Logger.info(rescore_builds(pkg, archs, args.priority))
Logger.info('') Logger.info("")
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -20,19 +20,23 @@
# #
# ################################################################## # ##################################################################
import optparse # pylint: disable=invalid-name
# pylint: enable=invalid-name
import argparse
import subprocess import subprocess
import sys import sys
from ubuntutools import getLogger from ubuntutools import getLogger
Logger = getLogger() Logger = getLogger()
def extract(iso, path): def extract(iso, path):
command = ['isoinfo', '-R', '-i', iso, '-x', path] command = ["isoinfo", "-R", "-i", iso, "-x", path]
pipe = subprocess.run(command, encoding='utf-8', pipe = subprocess.run(
stdout=subprocess.PIPE, command, check=False, encoding="utf-8", stdout=subprocess.PIPE, stderr=subprocess.PIPE
stderr=subprocess.PIPE) )
if pipe.returncode != 0: if pipe.returncode != 0:
sys.stderr.write(pipe.stderr) sys.stderr.write(pipe.stderr)
@ -42,22 +46,22 @@ def extract(iso, path):
def main(): def main():
desc = 'Given an ISO, %prog will display the Ubuntu version information' desc = "Given an ISO, %(prog)s will display the Ubuntu version information"
parser = optparse.OptionParser(usage='%prog [options] iso...', parser = argparse.ArgumentParser(usage="%(prog)s [options] iso...", description=desc)
description=desc) parser.add_argument("isos", nargs="*", help=argparse.SUPPRESS)
isos = parser.parse_args()[1] args = parser.parse_args()
err = False err = False
for iso in isos: for iso in args.isos:
if len(isos) > 1: if len(args.isos) > 1:
prefix = '%s:' % iso prefix = f"{iso}:"
else: else:
prefix = '' prefix = ""
version = extract(iso, '/.disk/info') version = extract(iso, "/.disk/info")
if len(version) == 0: if len(version) == 0:
Logger.error('%s does not appear to be an Ubuntu ISO' % iso) Logger.error("%s does not appear to be an Ubuntu ISO", iso)
err = True err = True
continue continue
@ -67,6 +71,6 @@ def main():
sys.exit(1) sys.exit(1)
if __name__ == '__main__': if __name__ == "__main__":
main() main()
sys.exit(0) sys.exit(0)

View File

@ -14,131 +14,159 @@
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
import optparse # pylint: disable=invalid-name
# pylint: enable=invalid-name
import argparse
import sys import sys
from ubuntutools.lp.lpapicache import (Launchpad, Distribution, PersonTeam, from ubuntutools import getLogger
Packageset, PackageNotFoundException, from ubuntutools.lp.lpapicache import (
SeriesNotFoundException) Distribution,
Launchpad,
PackageNotFoundException,
Packageset,
PersonTeam,
SeriesNotFoundException,
)
from ubuntutools.misc import split_release_pocket from ubuntutools.misc import split_release_pocket
from ubuntutools import getLogger
Logger = getLogger() Logger = getLogger()
def parse_arguments(): def parse_arguments():
'''Parse arguments and return (options, package)''' """Parse arguments and return (options, package)"""
parser = optparse.OptionParser('%prog [options] package') parser = argparse.ArgumentParser(usage="%(prog)s [options] package")
parser.add_option('-r', '--release', default=None, metavar='RELEASE', parser.add_argument(
help='Use RELEASE, rather than the current development ' "-r",
'release') "--release",
parser.add_option('-a', '--list-uploaders', metavar="RELEASE",
default=False, action='store_true', help="Use RELEASE, rather than the current development release",
help='List all the people/teams with upload rights') )
parser.add_option('-t', '--list-team-members', parser.add_argument(
default=False, action='store_true', "-a",
help='List all team members of teams with upload rights ' "--list-uploaders",
'(implies --list-uploaders)') action="store_true",
options, args = parser.parse_args() help="List all the people/teams with upload rights",
)
parser.add_argument(
"-t",
"--list-team-members",
action="store_true",
help="List all team members of teams with upload rights (implies --list-uploaders)",
)
parser.add_argument("package", help=argparse.SUPPRESS)
args = parser.parse_args()
if len(args) != 1: if args.list_team_members:
parser.error("One (and only one) package must be specified") args.list_uploaders = True
package = args[0]
if options.list_team_members: return args
options.list_uploaders = True
return (options, package)
def main(): def main():
'''Query upload permissions''' """Query upload permissions"""
options, package = parse_arguments() args = parse_arguments()
# Need to be logged in to see uploaders: # Need to be logged in to see uploaders:
Launchpad.login() Launchpad.login()
ubuntu = Distribution('ubuntu') ubuntu = Distribution("ubuntu")
archive = ubuntu.getArchive() archive = ubuntu.getArchive()
if options.release is None: if args.release is None:
options.release = ubuntu.getDevelopmentSeries().name args.release = ubuntu.getDevelopmentSeries().name
try: try:
release, pocket = split_release_pocket(options.release) release, pocket = split_release_pocket(args.release)
series = ubuntu.getSeries(release) series = ubuntu.getSeries(release)
except SeriesNotFoundException as e: except SeriesNotFoundException as e:
Logger.error(str(e)) Logger.error(str(e))
sys.exit(2) sys.exit(2)
try: try:
spph = archive.getSourcePackage(package) spph = archive.getSourcePackage(args.package)
except PackageNotFoundException as e: except PackageNotFoundException as e:
Logger.error(str(e)) Logger.error(str(e))
sys.exit(2) sys.exit(2)
component = spph.getComponent() component = spph.getComponent()
if (options.list_uploaders and (pocket != 'Release' or series.status in if args.list_uploaders and (
('Experimental', 'Active Development', 'Pre-release Freeze'))): pocket != "Release"
or series.status in ("Experimental", "Active Development", "Pre-release Freeze")
component_uploader = archive.getUploadersForComponent( ):
component_name=component)[0] component_uploader = archive.getUploadersForComponent(component_name=component)[0]
Logger.info("All upload permissions for %s:" % package) Logger.info("All upload permissions for %s:", args.package)
Logger.info("") Logger.info("")
Logger.info("Component (%s)" % component) Logger.info("Component (%s)", component)
Logger.info("============" + ("=" * len(component))) Logger.info("============%s", "=" * len(component))
print_uploaders([component_uploader], options.list_team_members) print_uploaders([component_uploader], args.list_team_members)
packagesets = sorted(Packageset.setsIncludingSource( packagesets = sorted(
distroseries=series, Packageset.setsIncludingSource(distroseries=series, sourcepackagename=args.package),
sourcepackagename=package), key=lambda p: p.name) key=lambda p: p.name,
)
if packagesets: if packagesets:
Logger.info("") Logger.info("")
Logger.info("Packagesets") Logger.info("Packagesets")
Logger.info("===========") Logger.info("===========")
for packageset in packagesets: for packageset in packagesets:
Logger.info("") Logger.info("")
Logger.info("%s:" % packageset.name) Logger.info("%s:", packageset.name)
print_uploaders(archive.getUploadersForPackageset( print_uploaders(
packageset=packageset), options.list_team_members) archive.getUploadersForPackageset(packageset=packageset),
args.list_team_members,
)
ppu_uploaders = archive.getUploadersForPackage( ppu_uploaders = archive.getUploadersForPackage(source_package_name=args.package)
source_package_name=package)
if ppu_uploaders: if ppu_uploaders:
Logger.info("") Logger.info("")
Logger.info("Per-Package-Uploaders") Logger.info("Per-Package-Uploaders")
Logger.info("=====================") Logger.info("=====================")
Logger.info("") Logger.info("")
print_uploaders(ppu_uploaders, options.list_team_members) print_uploaders(ppu_uploaders, args.list_team_members)
Logger.info("") Logger.info("")
if PersonTeam.me.canUploadPackage(archive, series, package, component, if PersonTeam.me.canUploadPackage(archive, series, args.package, component, pocket):
pocket): Logger.info("You can upload %s to %s.", args.package, args.release)
Logger.info("You can upload %s to %s." % (package, options.release))
else: else:
Logger.info("You can not upload %s to %s, yourself." % (package, options.release)) Logger.info("You can not upload %s to %s, yourself.", args.package, args.release)
if (series.status in ('Current Stable Release', 'Supported', 'Obsolete') if (
and pocket == 'Release'): series.status in ("Current Stable Release", "Supported", "Obsolete")
Logger.info("%s is in the '%s' state. You may want to query the %s-proposed pocket." % and pocket == "Release"
(release, series.status, release)) ):
Logger.info(
"%s is in the '%s' state. You may want to query the %s-proposed pocket.",
release,
series.status,
release,
)
else: else:
Logger.info("But you can still contribute to it via the sponsorship " Logger.info(
"process: https://wiki.ubuntu.com/SponsorshipProcess") "But you can still contribute to it via the sponsorship "
if not options.list_uploaders: "process: https://wiki.ubuntu.com/SponsorshipProcess"
Logger.info("To see who has the necessary upload rights, " )
"use the --list-uploaders option.") if not args.list_uploaders:
Logger.info(
"To see who has the necessary upload rights, "
"use the --list-uploaders option."
)
sys.exit(1) sys.exit(1)
def print_uploaders(uploaders, expand_teams=False, prefix=''): def print_uploaders(uploaders, expand_teams=False, prefix=""):
"""Given a list of uploaders, pretty-print them all """Given a list of uploaders, pretty-print them all
Each line is prefixed with prefix. Each line is prefixed with prefix.
If expand_teams is set, recurse, adding more spaces to prefix on each If expand_teams is set, recurse, adding more spaces to prefix on each
recursion. recursion.
""" """
for uploader in sorted(uploaders, key=lambda p: p.display_name): for uploader in sorted(uploaders, key=lambda p: p.display_name):
Logger.info("%s* %s (%s)%s" % Logger.info(
(prefix, uploader.display_name, uploader.name, "%s* %s (%s)%s",
' [team]' if uploader.is_team else '')) prefix,
uploader.display_name,
uploader.name,
" [team]" if uploader.is_team else "",
)
if expand_teams and uploader.is_team: if expand_teams and uploader.is_team:
print_uploaders(uploader.participants, True, prefix=prefix + ' ') print_uploaders(uploader.participants, True, prefix=prefix + " ")
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -7,8 +7,8 @@ import logging
import sys import sys
def getLogger(): def getLogger(): # pylint: disable=invalid-name
''' Get the logger instance for this module """Get the logger instance for this module
Quick guide for using this or not: if you want to call ubuntutools Quick guide for using this or not: if you want to call ubuntutools
module code and have its output print to stdout/stderr ONLY, you can module code and have its output print to stdout/stderr ONLY, you can
@ -33,12 +33,12 @@ def getLogger():
This should only be used by runnable scripts provided by the This should only be used by runnable scripts provided by the
ubuntu-dev-tools package, or other runnable scripts that want the behavior ubuntu-dev-tools package, or other runnable scripts that want the behavior
described above. described above.
''' """
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
logger.propagate = False logger.propagate = False
fmt = logging.Formatter('%(message)s') fmt = logging.Formatter("%(message)s")
stdout_handler = logging.StreamHandler(stream=sys.stdout) stdout_handler = logging.StreamHandler(stream=sys.stdout)
stdout_handler.setFormatter(fmt) stdout_handler.setFormatter(fmt)
@ -47,7 +47,7 @@ def getLogger():
stderr_handler = logging.StreamHandler(stream=sys.stderr) stderr_handler = logging.StreamHandler(stream=sys.stderr)
stderr_handler.setFormatter(fmt) stderr_handler.setFormatter(fmt)
stderr_handler.setLevel(logging.INFO+1) stderr_handler.setLevel(logging.INFO + 1)
logger.addHandler(stderr_handler) logger.addHandler(stderr_handler)
return logger return logger

File diff suppressed because it is too large Load Diff

View File

@ -18,10 +18,10 @@
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# #
import logging
import os import os
import subprocess import subprocess
import logging
Logger = logging.getLogger(__name__) Logger = logging.getLogger(__name__)
@ -31,20 +31,21 @@ def _build_preparation(result_directory):
os.makedirs(result_directory) os.makedirs(result_directory)
class Builder(object): class Builder:
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
cmd = ["dpkg-architecture", "-qDEB_BUILD_ARCH_CPU"] cmd = ["dpkg-architecture", "-qDEB_BUILD_ARCH_CPU"]
self.architecture = subprocess.check_output(cmd, encoding='utf-8').strip() self.architecture = subprocess.check_output(cmd, encoding="utf-8").strip()
def _build_failure(self, returncode, dsc_file): def _build_failure(self, returncode, dsc_file):
if returncode != 0: if returncode != 0:
Logger.error("Failed to build %s from source with %s." % Logger.error(
(os.path.basename(dsc_file), self.name)) "Failed to build %s from source with %s.", os.path.basename(dsc_file), self.name
)
return returncode return returncode
def exists_in_path(self): def exists_in_path(self):
for path in os.environ.get('PATH', os.defpath).split(os.pathsep): for path in os.environ.get("PATH", os.defpath).split(os.pathsep):
if os.path.isfile(os.path.join(path, self.name)): if os.path.isfile(os.path.join(path, self.name)):
return True return True
return False return False
@ -57,8 +58,7 @@ class Builder(object):
def _update_failure(self, returncode, dist): def _update_failure(self, returncode, dist):
if returncode != 0: if returncode != 0:
Logger.error("Failed to update %s chroot for %s." % Logger.error("Failed to update %s chroot for %s.", dist, self.name)
(dist, self.name))
return returncode return returncode
@ -68,19 +68,39 @@ class Pbuilder(Builder):
def build(self, dsc_file, dist, result_directory): def build(self, dsc_file, dist, result_directory):
_build_preparation(result_directory) _build_preparation(result_directory)
cmd = ["sudo", "-E", "ARCH=" + self.architecture, "DIST=" + dist, cmd = [
self.name, "--build", "sudo",
"--architecture", self.architecture, "--distribution", dist, "-E",
"--buildresult", result_directory, dsc_file] f"ARCH={self.architecture}",
Logger.debug(' '.join(cmd)) f"DIST={dist}",
self.name,
"--build",
"--architecture",
self.architecture,
"--distribution",
dist,
"--buildresult",
result_directory,
dsc_file,
]
Logger.debug(" ".join(cmd))
returncode = subprocess.call(cmd) returncode = subprocess.call(cmd)
return self._build_failure(returncode, dsc_file) return self._build_failure(returncode, dsc_file)
def update(self, dist): def update(self, dist):
cmd = ["sudo", "-E", "ARCH=" + self.architecture, "DIST=" + dist, cmd = [
self.name, "--update", "sudo",
"--architecture", self.architecture, "--distribution", dist] "-E",
Logger.debug(' '.join(cmd)) f"ARCH={self.architecture}",
f"DIST={dist}",
self.name,
"--update",
"--architecture",
self.architecture,
"--distribution",
dist,
]
Logger.debug(" ".join(cmd))
returncode = subprocess.call(cmd) returncode = subprocess.call(cmd)
return self._update_failure(returncode, dist) return self._update_failure(returncode, dist)
@ -91,15 +111,22 @@ class Pbuilderdist(Builder):
def build(self, dsc_file, dist, result_directory): def build(self, dsc_file, dist, result_directory):
_build_preparation(result_directory) _build_preparation(result_directory)
cmd = [self.name, dist, self.architecture, cmd = [
"build", dsc_file, "--buildresult", result_directory] self.name,
Logger.debug(' '.join(cmd)) dist,
self.architecture,
"build",
dsc_file,
"--buildresult",
result_directory,
]
Logger.debug(" ".join(cmd))
returncode = subprocess.call(cmd) returncode = subprocess.call(cmd)
return self._build_failure(returncode, dsc_file) return self._build_failure(returncode, dsc_file)
def update(self, dist): def update(self, dist):
cmd = [self.name, dist, self.architecture, "update"] cmd = [self.name, dist, self.architecture, "update"]
Logger.debug(' '.join(cmd)) Logger.debug(" ".join(cmd))
returncode = subprocess.call(cmd) returncode = subprocess.call(cmd)
return self._update_failure(returncode, dist) return self._update_failure(returncode, dist)
@ -111,41 +138,40 @@ class Sbuild(Builder):
def build(self, dsc_file, dist, result_directory): def build(self, dsc_file, dist, result_directory):
_build_preparation(result_directory) _build_preparation(result_directory)
workdir = os.getcwd() workdir = os.getcwd()
Logger.debug("cd " + result_directory) Logger.debug("cd %s", result_directory)
os.chdir(result_directory) os.chdir(result_directory)
cmd = ["sbuild", "--arch-all", "--dist=" + dist, cmd = ["sbuild", "--arch-all", f"--dist={dist}", f"--arch={self.architecture}", dsc_file]
"--arch=" + self.architecture, dsc_file] Logger.debug(" ".join(cmd))
Logger.debug(' '.join(cmd))
returncode = subprocess.call(cmd) returncode = subprocess.call(cmd)
Logger.debug("cd " + workdir) Logger.debug("cd %s", workdir)
os.chdir(workdir) os.chdir(workdir)
return self._build_failure(returncode, dsc_file) return self._build_failure(returncode, dsc_file)
def update(self, dist): def update(self, dist):
cmd = ["schroot", "--list"] cmd = ["schroot", "--list"]
Logger.debug(' '.join(cmd)) Logger.debug(" ".join(cmd))
process = subprocess.run(cmd, stdout=subprocess.PIPE, encoding='utf-8') process = subprocess.run(cmd, check=False, stdout=subprocess.PIPE, encoding="utf-8")
chroots, _ = process.stdout.strip().split() chroots, _ = process.stdout.strip().split()
if process.returncode != 0: if process.returncode != 0:
return process.returncode return process.returncode
params = {"dist": dist, "arch": self.architecture} params = {"dist": dist, "arch": self.architecture}
for chroot in ("%(dist)s-%(arch)s-sbuild-source", for chroot in (
"%(dist)s-sbuild-source", "%(dist)s-%(arch)s-sbuild-source",
"%(dist)s-%(arch)s-source", "%(dist)s-sbuild-source",
"%(dist)s-source"): "%(dist)s-%(arch)s-source",
"%(dist)s-source",
):
chroot = chroot % params chroot = chroot % params
if chroot in chroots: if chroot in chroots:
break break
else: else:
return 1 return 1
commands = [["sbuild-update"], commands = [["sbuild-update"], ["sbuild-distupgrade"], ["sbuild-clean", "-a", "-c"]]
["sbuild-distupgrade"],
["sbuild-clean", "-a", "-c"]]
for cmd in commands: for cmd in commands:
# pylint: disable=W0631 # pylint: disable=W0631
Logger.debug(' '.join(cmd) + " " + chroot) Logger.debug("%s %s", " ".join(cmd), chroot)
ret = subprocess.call(cmd + [chroot]) ret = subprocess.call(cmd + [chroot])
# pylint: enable=W0631 # pylint: enable=W0631
if ret != 0: if ret != 0:
@ -156,9 +182,9 @@ class Sbuild(Builder):
_SUPPORTED_BUILDERS = { _SUPPORTED_BUILDERS = {
"cowbuilder": lambda: Pbuilder("cowbuilder"), "cowbuilder": lambda: Pbuilder("cowbuilder"),
"cowbuilder-dist": lambda: Pbuilderdist("cowbuilder-dist"), "cowbuilder-dist": lambda: Pbuilderdist("cowbuilder-dist"),
"pbuilder": lambda: Pbuilder(), "pbuilder": Pbuilder,
"pbuilder-dist": lambda: Pbuilderdist(), "pbuilder-dist": Pbuilderdist,
"sbuild": lambda: Sbuild(), "sbuild": Sbuild,
} }
@ -170,5 +196,5 @@ def get_builder(name):
Logger.error("Builder doesn't appear to be installed: %s", name) Logger.error("Builder doesn't appear to be installed: %s", name)
else: else:
Logger.error("Unsupported builder specified: %s.", name) Logger.error("Unsupported builder specified: %s.", name)
Logger.error("Supported builders: %s", Logger.error("Supported builders: %s", ", ".join(sorted(_SUPPORTED_BUILDERS.keys())))
", ".join(sorted(_SUPPORTED_BUILDERS.keys()))) return None

View File

@ -15,39 +15,39 @@
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
import locale
import logging
import os import os
import pwd import pwd
import re import re
import shlex import shlex
import socket import socket
import sys import sys
import locale
import logging
Logger = logging.getLogger(__name__) Logger = logging.getLogger(__name__)
class UDTConfig(object): class UDTConfig:
"""Ubuntu Dev Tools configuration file (devscripts config file) and """Ubuntu Dev Tools configuration file (devscripts config file) and
environment variable parsing. environment variable parsing.
""" """
no_conf = False no_conf = False
# Package wide configuration variables. # Package wide configuration variables.
# These are reqired to be used by at least two scripts. # These are reqired to be used by at least two scripts.
defaults = { defaults = {
'BUILDER': 'pbuilder', "BUILDER": "pbuilder",
'DEBIAN_MIRROR': 'http://deb.debian.org/debian', "DEBIAN_MIRROR": "http://deb.debian.org/debian",
'DEBSEC_MIRROR': 'http://security.debian.org', "DEBSEC_MIRROR": "http://security.debian.org",
'DEBIAN_DDEBS_MIRROR': 'http://debug.mirrors.debian.org/debian-debug', "DEBIAN_DDEBS_MIRROR": "http://debug.mirrors.debian.org/debian-debug",
'LPINSTANCE': 'production', "LPINSTANCE": "production",
'MIRROR_FALLBACK': True, "MIRROR_FALLBACK": True,
'UBUNTU_MIRROR': 'http://archive.ubuntu.com/ubuntu', "UBUNTU_MIRROR": "http://archive.ubuntu.com/ubuntu",
'UBUNTU_PORTS_MIRROR': 'http://ports.ubuntu.com', "UBUNTU_PORTS_MIRROR": "http://ports.ubuntu.com",
'UBUNTU_INTERNAL_MIRROR': 'http://ftpmaster.internal/ubuntu', "UBUNTU_DDEBS_MIRROR": "http://ddebs.ubuntu.com",
'UBUNTU_DDEBS_MIRROR': 'http://ddebs.ubuntu.com', "UPDATE_BUILDER": False,
'UPDATE_BUILDER': False, "WORKDIR": None,
'WORKDIR': None, "KEYID": None,
'KEYID': None,
} }
# Populated from the configuration files: # Populated from the configuration files:
config = {} config = {}
@ -55,30 +55,32 @@ class UDTConfig(object):
def __init__(self, no_conf=False, prefix=None): def __init__(self, no_conf=False, prefix=None):
self.no_conf = no_conf self.no_conf = no_conf
if prefix is None: if prefix is None:
prefix = os.path.basename(sys.argv[0]).upper().replace('-', '_') prefix = os.path.basename(sys.argv[0]).upper().replace("-", "_")
self.prefix = prefix self.prefix = prefix
if not no_conf: if not no_conf:
self.config = self.parse_devscripts_config() self.config = self.parse_devscripts_config()
def parse_devscripts_config(self): @staticmethod
def parse_devscripts_config():
"""Read the devscripts configuration files, and return the values as a """Read the devscripts configuration files, and return the values as a
dictionary dictionary
""" """
config = {} config = {}
for filename in ('/etc/devscripts.conf', '~/.devscripts'): for filename in ("/etc/devscripts.conf", "~/.devscripts"):
try: try:
f = open(os.path.expanduser(filename), 'r') with open(os.path.expanduser(filename), "r", encoding="utf-8") as f:
content = f.read()
except IOError: except IOError:
continue continue
for line in f: try:
parsed = shlex.split(line, comments=True) tokens = shlex.split(content, comments=True)
if len(parsed) > 1: except ValueError as e:
Logger.warning('Cannot parse variable assignment in %s: %s', Logger.error("Error parsing %s: %s", filename, e)
getattr(f, 'name', '<config>'), line) continue
if len(parsed) >= 1 and '=' in parsed[0]: for token in tokens:
key, value = parsed[0].split('=', 1) if "=" in token:
key, value = token.split("=", 1)
config[key] = value config[key] = value
f.close()
return config return config
def get_value(self, key, default=None, boolean=False, compat_keys=()): def get_value(self, key, default=None, boolean=False, compat_keys=()):
@ -95,9 +97,9 @@ class UDTConfig(object):
if default is None and key in self.defaults: if default is None and key in self.defaults:
default = self.defaults[key] default = self.defaults[key]
keys = [self.prefix + '_' + key] keys = [f"{self.prefix}_{key}"]
if key in self.defaults: if key in self.defaults:
keys.append('UBUNTUTOOLS_' + key) keys.append(f"UBUNTUTOOLS_{key}")
keys += compat_keys keys += compat_keys
for k in keys: for k in keys:
@ -105,16 +107,19 @@ class UDTConfig(object):
if k in store: if k in store:
value = store[k] value = store[k]
if boolean: if boolean:
if value in ('yes', 'no'): if value in ("yes", "no"):
value = value == 'yes' value = value == "yes"
else: else:
continue continue
if k in compat_keys: if k in compat_keys:
replacements = self.prefix + '_' + key replacements = f"{self.prefix}_{key}"
if key in self.defaults: if key in self.defaults:
replacements += 'or UBUNTUTOOLS_' + key replacements += f"or UBUNTUTOOLS_{key}"
Logger.warning('Using deprecated configuration variable %s. ' Logger.warning(
'You should use %s.', k, replacements) "Using deprecated configuration variable %s. You should use %s.",
k,
replacements,
)
return value return value
return default return default
@ -132,7 +137,7 @@ def ubu_email(name=None, email=None, export=True):
Return name, email. Return name, email.
""" """
name_email_re = re.compile(r'^\s*(.+?)\s*<(.+@.+)>\s*$') name_email_re = re.compile(r"^\s*(.+?)\s*<(.+@.+)>\s*$")
if email: if email:
match = name_email_re.match(email) match = name_email_re.match(email)
@ -140,11 +145,16 @@ def ubu_email(name=None, email=None, export=True):
name = match.group(1) name = match.group(1)
email = match.group(2) email = match.group(2)
if export and not name and not email and 'UBUMAIL' not in os.environ: if export and not name and not email and "UBUMAIL" not in os.environ:
export = False export = False
for var, target in (('UBUMAIL', 'email'), ('DEBFULLNAME', 'name'), ('DEBEMAIL', 'email'), for var, target in (
('EMAIL', 'email'), ('NAME', 'name')): ("UBUMAIL", "email"),
("DEBFULLNAME", "name"),
("DEBEMAIL", "email"),
("EMAIL", "email"),
("NAME", "name"),
):
if name and email: if name and email:
break break
if var in os.environ: if var in os.environ:
@ -154,30 +164,30 @@ def ubu_email(name=None, email=None, export=True):
name = match.group(1) name = match.group(1)
if not email: if not email:
email = match.group(2) email = match.group(2)
elif target == 'name' and not name: elif target == "name" and not name:
name = os.environ[var].strip() name = os.environ[var].strip()
elif target == 'email' and not email: elif target == "email" and not email:
email = os.environ[var].strip() email = os.environ[var].strip()
if not name: if not name:
gecos_name = pwd.getpwuid(os.getuid()).pw_gecos.split(',')[0].strip() gecos_name = pwd.getpwuid(os.getuid()).pw_gecos.split(",")[0].strip()
if gecos_name: if gecos_name:
name = gecos_name name = gecos_name
if not email: if not email:
mailname = socket.getfqdn() mailname = socket.getfqdn()
if os.path.isfile('/etc/mailname'): if os.path.isfile("/etc/mailname"):
mailname = open('/etc/mailname', 'r').read().strip() mailname = open("/etc/mailname", "r", encoding="utf-8").read().strip()
email = pwd.getpwuid(os.getuid()).pw_name + '@' + mailname email = f"{pwd.getpwuid(os.getuid()).pw_name}@{mailname}"
if export: if export:
os.environ['DEBFULLNAME'] = name os.environ["DEBFULLNAME"] = name
os.environ['DEBEMAIL'] = email os.environ["DEBEMAIL"] = email
# decode env var or gecos raw string with the current locale's encoding # decode env var or gecos raw string with the current locale's encoding
encoding = locale.getdefaultlocale()[1] encoding = locale.getlocale()[1]
if not encoding: if not encoding:
encoding = 'utf-8' encoding = "utf-8"
if name and isinstance(name, bytes): if name and isinstance(name, bytes):
name = name.decode(encoding) name = name.decode(encoding)
return name, email return name, email

View File

@ -2,5 +2,5 @@
# ubuntu-dev-tools Launchpad Python modules. # ubuntu-dev-tools Launchpad Python modules.
# #
service = 'production' SERVICE = "production"
api_version = 'devel' API_VERSION = "devel"

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,26 @@
class PackageNotFoundException(BaseException): class PackageNotFoundException(BaseException):
""" Thrown when a package is not found """ """Thrown when a package is not found"""
pass
class SeriesNotFoundException(BaseException): class SeriesNotFoundException(BaseException):
""" Thrown when a distroseries is not found """ """Thrown when a distroseries is not found"""
pass
class PocketDoesNotExistError(Exception): class PocketDoesNotExistError(Exception):
'''Raised when a invalid pocket is used.''' """Raised when a invalid pocket is used."""
pass
class ArchiveNotFoundException(BaseException): class ArchiveNotFoundException(BaseException):
""" Thrown when an archive for a distibution is not found """ """Thrown when an archive for a distibution is not found"""
pass
class AlreadyLoggedInError(Exception): class AlreadyLoggedInError(Exception):
'''Raised when a second login is attempted.''' """Raised when a second login is attempted."""
pass
class ArchSeriesNotFoundException(BaseException): class ArchSeriesNotFoundException(BaseException):
"""Thrown when a distroarchseries is not found.""" """Thrown when a distroarchseries is not found."""
pass
class InvalidDistroValueError(ValueError): class InvalidDistroValueError(ValueError):
""" Thrown when distro value is invalid """ """Thrown when distro value is invalid"""
pass

View File

@ -22,51 +22,49 @@
# #
# ################################################################## # ##################################################################
import distro_info
import hashlib import hashlib
import locale import locale
import logging
import os import os
import requests
import shutil import shutil
import sys import sys
import tempfile import tempfile
from contextlib import suppress from contextlib import suppress
from pathlib import Path from pathlib import Path
from subprocess import check_output, CalledProcessError from subprocess import CalledProcessError, check_output
from urllib.parse import urlparse from urllib.parse import urlparse
import distro_info
import requests
from ubuntutools.lp.udtexceptions import PocketDoesNotExistError from ubuntutools.lp.udtexceptions import PocketDoesNotExistError
import logging
Logger = logging.getLogger(__name__) Logger = logging.getLogger(__name__)
DEFAULT_POCKETS = ('Release', 'Security', 'Updates', 'Proposed') DEFAULT_POCKETS = ("Release", "Security", "Updates", "Proposed")
POCKETS = DEFAULT_POCKETS + ('Backports',) POCKETS = DEFAULT_POCKETS + ("Backports",)
DEFAULT_STATUSES = ('Pending', 'Published') DEFAULT_STATUSES = ("Pending", "Published")
STATUSES = DEFAULT_STATUSES + ('Superseded', 'Deleted', 'Obsolete') STATUSES = DEFAULT_STATUSES + ("Superseded", "Deleted", "Obsolete")
UPLOAD_QUEUE_STATUSES = ('New', 'Unapproved', 'Accepted', 'Done', 'Rejected') UPLOAD_QUEUE_STATUSES = ("New", "Unapproved", "Accepted", "Done", "Rejected")
DOWNLOAD_BLOCKSIZE_DEFAULT = 8192 DOWNLOAD_BLOCKSIZE_DEFAULT = 8192
_system_distribution_chain = [] _SYSTEM_DISTRIBUTION_CHAIN: list[str] = []
class DownloadError(Exception): class DownloadError(Exception):
"Unable to pull a source package" "Unable to pull a source package"
pass
class NotFoundError(DownloadError): class NotFoundError(DownloadError):
"Source package not found" "Source package not found"
pass
def system_distribution_chain(): def system_distribution_chain():
""" system_distribution_chain() -> [string] """system_distribution_chain() -> [string]
Detect the system's distribution as well as all of its parent Detect the system's distribution as well as all of its parent
distributions and return them as a list of strings, with the distributions and return them as a list of strings, with the
@ -74,31 +72,36 @@ def system_distribution_chain():
the distribution chain can't be determined, print an error message the distribution chain can't be determined, print an error message
and return an empty list. and return an empty list.
""" """
global _system_distribution_chain if len(_SYSTEM_DISTRIBUTION_CHAIN) == 0:
if len(_system_distribution_chain) == 0:
try: try:
vendor = check_output(('dpkg-vendor', '--query', 'Vendor'), vendor = check_output(("dpkg-vendor", "--query", "Vendor"), encoding="utf-8").strip()
encoding='utf-8').strip() _SYSTEM_DISTRIBUTION_CHAIN.append(vendor)
_system_distribution_chain.append(vendor)
except CalledProcessError: except CalledProcessError:
Logger.error('Could not determine what distribution you are running.') Logger.error("Could not determine what distribution you are running.")
return [] return []
while True: while True:
try: try:
parent = check_output(( parent = check_output(
'dpkg-vendor', '--vendor', _system_distribution_chain[-1], (
'--query', 'Parent'), encoding='utf-8').strip() "dpkg-vendor",
"--vendor",
_SYSTEM_DISTRIBUTION_CHAIN[-1],
"--query",
"Parent",
),
encoding="utf-8",
).strip()
except CalledProcessError: except CalledProcessError:
# Vendor has no parent # Vendor has no parent
break break
_system_distribution_chain.append(parent) _SYSTEM_DISTRIBUTION_CHAIN.append(parent)
return _system_distribution_chain return _SYSTEM_DISTRIBUTION_CHAIN
def system_distribution(): def system_distribution():
""" system_distro() -> string """system_distro() -> string
Detect the system's distribution and return it as a string. If the Detect the system's distribution and return it as a string. If the
name of the distribution can't be determined, print an error message name of the distribution can't be determined, print an error message
@ -108,42 +111,40 @@ def system_distribution():
def host_architecture(): def host_architecture():
""" host_architecture -> string """host_architecture -> string
Detect the host's architecture and return it as a string. If the Detect the host's architecture and return it as a string. If the
architecture can't be determined, print an error message and return None. architecture can't be determined, print an error message and return None.
""" """
try: try:
arch = check_output(('dpkg', '--print-architecture'), arch = check_output(("dpkg", "--print-architecture"), encoding="utf-8").strip()
encoding='utf-8').strip()
except CalledProcessError: except CalledProcessError:
arch = None arch = None
if not arch or 'not found' in arch: if not arch or "not found" in arch:
Logger.error('Not running on a Debian based system; ' Logger.error("Not running on a Debian based system; could not detect its architecture.")
'could not detect its architecture.')
return None return None
return arch return arch
def readlist(filename, uniq=True): def readlist(filename, uniq=True):
""" readlist(filename, uniq) -> list """readlist(filename, uniq) -> list
Read a list of words from the indicated file. If 'uniq' is True, filter Read a list of words from the indicated file. If 'uniq' is True, filter
out duplicated words. out duplicated words.
""" """
p = Path(filename) path = Path(filename)
if not p.is_file(): if not path.is_file():
Logger.error(f'File {p} does not exist.') Logger.error("File %s does not exist.", path)
return False return False
content = p.read_text().replace('\n', ' ').replace(',', ' ') content = path.read_text(encoding="utf-8").replace("\n", " ").replace(",", " ")
if not content.strip(): if not content.strip():
Logger.error(f'File {p} is empty.') Logger.error("File %s is empty.", path)
return False return False
items = [item for item in content.split() if item] items = [item for item in content.split() if item]
@ -154,42 +155,44 @@ def readlist(filename, uniq=True):
return items return items
def split_release_pocket(release, default='Release'): def split_release_pocket(release, default="Release"):
'''Splits the release and pocket name. """Splits the release and pocket name.
If the argument doesn't contain a pocket name then the 'Release' pocket If the argument doesn't contain a pocket name then the 'Release' pocket
is assumed. is assumed.
Returns the release and pocket name. Returns the release and pocket name.
''' """
pocket = default pocket = default
if release is None: if release is None:
raise ValueError('No release name specified') raise ValueError("No release name specified")
if '-' in release: if "-" in release:
(release, pocket) = release.rsplit('-', 1) (release, pocket) = release.rsplit("-", 1)
pocket = pocket.capitalize() pocket = pocket.capitalize()
if pocket not in POCKETS: if pocket not in POCKETS:
raise PocketDoesNotExistError("Pocket '%s' does not exist." % pocket) raise PocketDoesNotExistError(f"Pocket '{pocket}' does not exist.")
return (release, pocket) return (release, pocket)
def require_utf8(): def require_utf8():
'''Can be called by programs that only function in UTF-8 locales''' """Can be called by programs that only function in UTF-8 locales"""
if locale.getpreferredencoding() != 'UTF-8': if locale.getpreferredencoding() != "UTF-8":
Logger.error("This program only functions in a UTF-8 locale. Aborting.") Logger.error("This program only functions in a UTF-8 locale. Aborting.")
sys.exit(1) sys.exit(1)
_vendor_to_distroinfo = {"Debian": distro_info.DebianDistroInfo, _vendor_to_distroinfo = {
"Ubuntu": distro_info.UbuntuDistroInfo} "Debian": distro_info.DebianDistroInfo,
"Ubuntu": distro_info.UbuntuDistroInfo,
}
def vendor_to_distroinfo(vendor): def vendor_to_distroinfo(vendor):
""" vendor_to_distroinfo(string) -> DistroInfo class """vendor_to_distroinfo(string) -> DistroInfo class
Convert a string name of a distribution into a DistroInfo subclass Convert a string name of a distribution into a DistroInfo subclass
representing that distribution, or None if the distribution is representing that distribution, or None if the distribution is
@ -199,7 +202,7 @@ def vendor_to_distroinfo(vendor):
def codename_to_distribution(codename): def codename_to_distribution(codename):
""" codename_to_distribution(string) -> string """codename_to_distribution(string) -> string
Finds a given release codename in your distribution's genaology Finds a given release codename in your distribution's genaology
(i.e. looking at the current distribution and its parents), or (i.e. looking at the current distribution and its parents), or
@ -212,10 +215,11 @@ def codename_to_distribution(codename):
if info().valid(codename): if info().valid(codename):
return distro return distro
return None
def verify_file_checksums(pathname, checksums={}, size=0): def verify_file_checksums(pathname, checksums=None, size=0):
""" verify checksums of file """verify checksums of file
Any failure will log an error. Any failure will log an error.
@ -228,35 +232,39 @@ def verify_file_checksums(pathname, checksums={}, size=0):
Returns True if all checks pass, False otherwise Returns True if all checks pass, False otherwise
""" """
p = Path(pathname) if checksums is None:
checksums = {}
path = Path(pathname)
if not p.is_file(): if not path.is_file():
Logger.error(f'File {p} not found') Logger.error("File %s not found", path)
return False return False
filesize = p.stat().st_size filesize = path.stat().st_size
if size and size != filesize: if size and size != filesize:
Logger.error(f'File {p} incorrect size, got {filesize} expected {size}') Logger.error("File %s incorrect size, got %s expected %s", path, filesize, size)
return False return False
for (alg, checksum) in checksums.items(): for alg, checksum in checksums.items():
h = hashlib.new(alg) hash_ = hashlib.new(alg)
with p.open('rb') as f: with path.open("rb") as f:
while True: while True:
block = f.read(h.block_size) block = f.read(hash_.block_size)
if len(block) == 0: if len(block) == 0:
break break
h.update(block) hash_.update(block)
digest = h.hexdigest() digest = hash_.hexdigest()
if digest == checksum: if digest == checksum:
Logger.debug(f'File {p} checksum ({alg}) verified: {checksum}') Logger.debug("File %s checksum (%s) verified: %s", path, alg, checksum)
else: else:
Logger.error(f'File {p} checksum ({alg}) mismatch: got {digest} expected {checksum}') Logger.error(
"File %s checksum (%s) mismatch: got %s expected %s", path, alg, digest, checksum
)
return False return False
return True return True
def verify_file_checksum(pathname, alg, checksum, size=0): def verify_file_checksum(pathname, alg, checksum, size=0):
""" verify checksum of file """verify checksum of file
pathname: str or Path pathname: str or Path
full path to file full path to file
@ -273,7 +281,7 @@ def verify_file_checksum(pathname, alg, checksum, size=0):
def extract_authentication(url): def extract_authentication(url):
""" Remove plaintext authentication data from a URL """Remove plaintext authentication data from a URL
If the URL has a username:password in its netloc, this removes it If the URL has a username:password in its netloc, this removes it
and returns the remaining URL, along with the username and password and returns the remaining URL, along with the username and password
@ -282,14 +290,18 @@ def extract_authentication(url):
This returns a tuple in the form (url, username, password) This returns a tuple in the form (url, username, password)
""" """
u = urlparse(url) components = urlparse(url)
if u.username or u.password: if components.username or components.password:
return (u._replace(netloc=u.hostname).geturl(), u.username, u.password) return (
components._replace(netloc=components.hostname).geturl(),
components.username,
components.password,
)
return (url, None, None) return (url, None, None)
def download(src, dst, size=0, *, blocksize=DOWNLOAD_BLOCKSIZE_DEFAULT): def download(src, dst, size=0, *, blocksize=DOWNLOAD_BLOCKSIZE_DEFAULT):
""" download/copy a file/url to local file """download/copy a file/url to local file
src: str or Path src: str or Path
Source to copy from (file path or url) Source to copy from (file path or url)
@ -315,128 +327,148 @@ def download(src, dst, size=0, *, blocksize=DOWNLOAD_BLOCKSIZE_DEFAULT):
dst = dst / Path(parsedsrc.path).name dst = dst / Path(parsedsrc.path).name
# Copy if src is a local file # Copy if src is a local file
if parsedsrc.scheme in ['', 'file']: if parsedsrc.scheme in ["", "file"]:
src = Path(parsedsrc.path).expanduser().resolve() src = Path(parsedsrc.path).expanduser().resolve()
if src != parsedsrc.path: if src != parsedsrc.path:
Logger.info(f'Parsed {parsedsrc.path} as {src}') Logger.info("Parsed %s as %s", parsedsrc.path, src)
if not src.exists(): if not src.exists():
raise NotFoundError(f'Source file {src} not found') raise NotFoundError(f"Source file {src} not found")
if dst.exists(): if dst.exists():
if src.samefile(dst): if src.samefile(dst):
Logger.info(f'Using existing file {dst}') Logger.info("Using existing file %s", dst)
return dst return dst
Logger.info(f'Replacing existing file {dst}') Logger.info("Replacing existing file %s", dst)
Logger.info(f'Copying file {src} to {dst}') Logger.info("Copying file %s to %s", src, dst)
shutil.copyfile(src, dst) shutil.copyfile(src, dst)
return dst return dst
(src, username, password) = extract_authentication(src) (src, username, password) = extract_authentication(src)
auth = (username, password) if username or password else None auth = (username, password) if username or password else None
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as tmpdir:
tmpdst = Path(d) / 'dst' tmpdst = Path(tmpdir) / "dst"
try: try:
with requests.get(src, stream=True, auth=auth) as fsrc, tmpdst.open('wb') as fdst: # We must use "Accept-Encoding: identity" so that Launchpad doesn't
fsrc.raise_for_status() # compress changes files. See LP: #2025748.
_download(fsrc, fdst, size, blocksize=blocksize) with requests.get(
except requests.exceptions.HTTPError as e: src, stream=True, timeout=60, auth=auth, headers={"accept-encoding": "identity"}
if e.response is not None and e.response.status_code == 404: ) as fsrc:
raise NotFoundError(f'URL {src} not found: {e}') with tmpdst.open("wb") as fdst:
raise DownloadError(e) fsrc.raise_for_status()
except requests.exceptions.ConnectionError as e: _download(fsrc, fdst, size, blocksize=blocksize)
except requests.exceptions.HTTPError as error:
if error.response is not None and error.response.status_code == 404:
raise NotFoundError(f"URL {src} not found: {error}") from error
raise DownloadError(error) from error
except requests.exceptions.ConnectionError as error:
# This is likely a archive hostname that doesn't resolve, like 'ftpmaster.internal' # This is likely a archive hostname that doesn't resolve, like 'ftpmaster.internal'
raise NotFoundError(f'URL {src} not found: {e}') raise NotFoundError(f"URL {src} not found: {error}") from error
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as error:
raise DownloadError(e) raise DownloadError(error) from error
shutil.move(tmpdst, dst) shutil.move(tmpdst, dst)
return dst return dst
class _StderrProgressBar(object): class _StderrProgressBar:
BAR_WIDTH_MIN = 40 BAR_WIDTH_MIN = 40
BAR_WIDTH_DEFAULT = 60 BAR_WIDTH_DEFAULT = 60
def __init__(self, max_width): def __init__(self, max_width):
self.full_width = min(max_width, self.BAR_WIDTH_DEFAULT) self.full_width = min(max_width, self.BAR_WIDTH_DEFAULT)
self.width = self.full_width - len('[] 99%') self.width = self.full_width - len("[] 99%")
self.show_progress = self.full_width >= self.BAR_WIDTH_MIN self.show_progress = self.full_width >= self.BAR_WIDTH_MIN
def update(self, progress, total): def update(self, progress, total):
if not self.show_progress: if not self.show_progress:
return return
pct = progress * 100 // total pct = progress * 100 // total
pctstr = f'{pct:>3}%' pctstr = f"{pct:>3}%"
barlen = self.width * pct // 100 barlen = self.width * pct // 100
barstr = '=' * barlen barstr = "=" * barlen
barstr = barstr[:-1] + '>' barstr = f"{barstr[:-1]}>"
barstr = barstr.ljust(self.width) barstr = barstr.ljust(self.width)
fullstr = f'\r[{barstr}]{pctstr}' fullstr = f"\r[{barstr}]{pctstr}"
sys.stderr.write(fullstr) sys.stderr.write(fullstr)
sys.stderr.flush() sys.stderr.flush()
def finish(self): def finish(self):
if not self.show_progress: if not self.show_progress:
return return
sys.stderr.write('\n') sys.stderr.write("\n")
sys.stderr.flush() sys.stderr.flush()
def _download(fsrc, fdst, size, *, blocksize): def _download(fsrc, fdst, size, *, blocksize):
""" helper method to download src to dst using requests library. """ """helper method to download src to dst using requests library."""
url = fsrc.url url = fsrc.url
Logger.debug(f'Using URL: {url}') Logger.debug("Using URL: %s", url)
if not size: if not size:
with suppress(AttributeError, TypeError, ValueError): with suppress(AttributeError, TypeError, ValueError):
size = int(fsrc.headers.get('Content-Length')) size = int(fsrc.headers.get("Content-Length"))
parsed = urlparse(url) parsed = urlparse(url)
filename = Path(parsed.path).name filename = Path(parsed.path).name
hostname = parsed.hostname hostname = parsed.hostname
sizemb = ' (%0.3f MiB)' % (size / 1024.0 / 1024) if size else '' sizemb = f" ({size / 1024.0 / 1024:0.3f} MiB)" if size else ""
Logger.info(f'Downloading {filename} from {hostname}{sizemb}') Logger.info("Downloading %s from %s%s", filename, hostname, sizemb)
# Don't show progress if: # Don't show progress if:
# logging INFO is suppressed # logging INFO is suppressed
# stderr isn't a tty # stderr isn't a tty
# we don't know the total file size # we don't know the total file size
# the file is content-encoded (i.e. compressed) # the file is content-encoded (i.e. compressed)
show_progress = all((Logger.isEnabledFor(logging.INFO), show_progress = all(
sys.stderr.isatty(), (
size > 0, Logger.isEnabledFor(logging.INFO),
'Content-Encoding' not in fsrc.headers)) sys.stderr.isatty(),
size > 0,
"Content-Encoding" not in fsrc.headers,
)
)
terminal_width = 0 terminal_width = 0
if show_progress: if show_progress:
try: try:
terminal_width = os.get_terminal_size(sys.stderr.fileno()).columns terminal_width = os.get_terminal_size(sys.stderr.fileno()).columns
except Exception as e: except Exception as e: # pylint: disable=broad-except
Logger.error(f'Error finding stderr width, suppressing progress bar: {e}') Logger.error("Error finding stderr width, suppressing progress bar: %s", e)
progress_bar = _StderrProgressBar(max_width=terminal_width) progress_bar = _StderrProgressBar(max_width=terminal_width)
downloaded = 0 downloaded = 0
try: try:
for block in fsrc.iter_content(blocksize): while True:
# We use fsrc.raw so that compressed files stay compressed as we
# write them to disk. For example, if this is a .diff.gz, then it
# needs to remain compressed and unmodified to remain valid as part
# of a source package later, even though Launchpad sends
# "Content-Encoding: gzip" and the requests library therefore would
# want to decompress it. See LP: #2025748.
block = fsrc.raw.read(blocksize)
if not block:
break
fdst.write(block) fdst.write(block)
downloaded += len(block) downloaded += len(block)
progress_bar.update(downloaded, size) progress_bar.update(downloaded, size)
finally: finally:
progress_bar.finish() progress_bar.finish()
if size and size > downloaded: if size and size > downloaded:
Logger.error('Partial download: %0.3f MiB of %0.3f MiB' % Logger.error(
(downloaded / 1024.0 / 1024, "Partial download: %0.3f MiB of %0.3f MiB",
size / 1024.0 / 1024)) downloaded / 1024.0 / 1024,
size / 1024.0 / 1024,
)
def _download_text(src, binary, *, blocksize): def _download_text(src, binary, *, blocksize):
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as tmpdir:
dst = Path(d) / 'dst' dst = Path(tmpdir) / "dst"
download(src, dst, blocksize=blocksize) download(src, dst, blocksize=blocksize)
return dst.read_bytes() if binary else dst.read_text() return dst.read_bytes() if binary else dst.read_text()
def download_text(src, mode=None, *, blocksize=DOWNLOAD_BLOCKSIZE_DEFAULT): def download_text(src, mode=None, *, blocksize=DOWNLOAD_BLOCKSIZE_DEFAULT):
""" Return the text content of a downloaded file """Return the text content of a downloaded file
src: str or Path src: str or Path
Source to copy from (file path or url) Source to copy from (file path or url)
@ -449,9 +481,9 @@ def download_text(src, mode=None, *, blocksize=DOWNLOAD_BLOCKSIZE_DEFAULT):
Returns text content of downloaded file Returns text content of downloaded file
""" """
return _download_text(src, binary='b' in (mode or ''), blocksize=blocksize) return _download_text(src, binary="b" in (mode or ""), blocksize=blocksize)
def download_bytes(src, *, blocksize=DOWNLOAD_BLOCKSIZE_DEFAULT): def download_bytes(src, *, blocksize=DOWNLOAD_BLOCKSIZE_DEFAULT):
""" Same as download_text() but returns bytes """ """Same as download_text() but returns bytes"""
return _download_text(src, binary=True, blocksize=blocksize) return _download_text(src, binary=True, blocksize=blocksize)

View File

@ -22,54 +22,58 @@
# ################################################################## # ##################################################################
import errno
import logging
import os import os
import re import re
import sys
import errno
import subprocess import subprocess
import sys
from argparse import ArgumentParser from argparse import ArgumentParser
from distro_info import DebianDistroInfo
from urllib.parse import urlparse from urllib.parse import urlparse
from ubuntutools.archive import (UbuntuSourcePackage, DebianSourcePackage, from distro_info import DebianDistroInfo
UbuntuCloudArchiveSourcePackage,
PersonalPackageArchiveSourcePackage)
from ubuntutools.config import UDTConfig
from ubuntutools.lp.lpapicache import (Distribution, Launchpad)
from ubuntutools.lp.udtexceptions import (AlreadyLoggedInError,
SeriesNotFoundException,
PackageNotFoundException,
PocketDoesNotExistError,
InvalidDistroValueError)
from ubuntutools.misc import (split_release_pocket,
host_architecture,
download,
UPLOAD_QUEUE_STATUSES,
STATUSES)
# by default we use standard logging.getLogger() and only use # by default we use standard logging.getLogger() and only use
# ubuntutools.getLogger() in PullPkg().main() # ubuntutools.getLogger() in PullPkg().main()
from ubuntutools import getLogger as ubuntutools_getLogger from ubuntutools import getLogger as ubuntutools_getLogger
import logging from ubuntutools.archive import (
DebianSourcePackage,
PersonalPackageArchiveSourcePackage,
UbuntuCloudArchiveSourcePackage,
UbuntuSourcePackage,
)
from ubuntutools.config import UDTConfig
from ubuntutools.lp.lpapicache import Distribution, Launchpad
from ubuntutools.lp.udtexceptions import (
AlreadyLoggedInError,
InvalidDistroValueError,
PackageNotFoundException,
PocketDoesNotExistError,
SeriesNotFoundException,
)
from ubuntutools.misc import (
STATUSES,
UPLOAD_QUEUE_STATUSES,
download,
host_architecture,
split_release_pocket,
)
Logger = logging.getLogger(__name__) Logger = logging.getLogger(__name__)
PULL_SOURCE = 'source' PULL_SOURCE = "source"
PULL_DEBS = 'debs' PULL_DEBS = "debs"
PULL_DDEBS = 'ddebs' PULL_DDEBS = "ddebs"
PULL_UDEBS = 'udebs' PULL_UDEBS = "udebs"
PULL_LIST = 'list' PULL_LIST = "list"
VALID_PULLS = [PULL_SOURCE, PULL_DEBS, PULL_DDEBS, PULL_UDEBS, PULL_LIST] VALID_PULLS = [PULL_SOURCE, PULL_DEBS, PULL_DDEBS, PULL_UDEBS, PULL_LIST]
VALID_BINARY_PULLS = [PULL_DEBS, PULL_DDEBS, PULL_UDEBS] VALID_BINARY_PULLS = [PULL_DEBS, PULL_DDEBS, PULL_UDEBS]
DISTRO_DEBIAN = 'debian' DISTRO_DEBIAN = "debian"
DISTRO_UBUNTU = 'ubuntu' DISTRO_UBUNTU = "ubuntu"
DISTRO_UCA = 'uca' DISTRO_UCA = "uca"
DISTRO_PPA = 'ppa' DISTRO_PPA = "ppa"
DISTRO_PKG_CLASS = { DISTRO_PKG_CLASS = {
DISTRO_DEBIAN: DebianSourcePackage, DISTRO_DEBIAN: DebianSourcePackage,
@ -81,12 +85,12 @@ VALID_DISTROS = DISTRO_PKG_CLASS.keys()
class InvalidPullValueError(ValueError): class InvalidPullValueError(ValueError):
""" Thrown when --pull value is invalid """ """Thrown when --pull value is invalid"""
pass
class PullPkg(object): class PullPkg:
"""Class used to pull file(s) associated with a specific package""" """Class used to pull file(s) associated with a specific package"""
@classmethod @classmethod
def main(cls, *args, **kwargs): def main(cls, *args, **kwargs):
"""For use by stand-alone cmdline scripts. """For use by stand-alone cmdline scripts.
@ -101,59 +105,74 @@ class PullPkg(object):
unexpected errors will flow up to the caller. unexpected errors will flow up to the caller.
On success, this simply returns. On success, this simply returns.
""" """
Logger = ubuntutools_getLogger() logger = ubuntutools_getLogger()
try: try:
cls(*args, **kwargs).pull() cls(*args, **kwargs).pull()
return return
except KeyboardInterrupt: except KeyboardInterrupt:
Logger.info('User abort.') logger.info("User abort.")
except (PackageNotFoundException, SeriesNotFoundException, except (
PocketDoesNotExistError, InvalidDistroValueError, PackageNotFoundException,
InvalidPullValueError) as e: SeriesNotFoundException,
Logger.error(str(e)) PocketDoesNotExistError,
InvalidDistroValueError,
InvalidPullValueError,
) as error:
logger.error(str(error))
sys.exit(errno.ENOENT) sys.exit(errno.ENOENT)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs): # pylint: disable=unused-argument
self._default_pull = kwargs.get('pull') self._default_pull = kwargs.get("pull")
self._default_distro = kwargs.get('distro') self._default_distro = kwargs.get("distro")
self._default_arch = kwargs.get('arch', host_architecture()) self._default_arch = kwargs.get("arch", host_architecture())
def parse_args(self, args): def parse_args(self, args):
args = args[:] if args is None:
args = sys.argv[1:]
help_default_pull = "What to pull: " + ", ".join(VALID_PULLS) help_default_pull = "What to pull: " + ", ".join(VALID_PULLS)
if self._default_pull: if self._default_pull:
help_default_pull += (" (default: %s)" % self._default_pull) help_default_pull += f" (default: {self._default_pull})"
help_default_distro = "Pull from: " + ", ".join(VALID_DISTROS) help_default_distro = "Pull from: " + ", ".join(VALID_DISTROS)
if self._default_distro: if self._default_distro:
help_default_distro += (" (default: %s)" % self._default_distro) help_default_distro += f" (default: {self._default_distro})"
help_default_arch = ("Get binary packages for arch") help_default_arch = "Get binary packages for arch"
help_default_arch += ("(default: %s)" % self._default_arch) help_default_arch += f"(default: {self._default_arch})"
# use add_help=False because we do parse_known_args() below, and if # use add_help=False because we do parse_known_args() below, and if
# that sees --help then it exits immediately # that sees --help then it exits immediately
parser = ArgumentParser(add_help=False) parser = ArgumentParser(add_help=False)
parser.add_argument('-L', '--login', action='store_true', parser.add_argument("-L", "--login", action="store_true", help="Login to Launchpad")
help="Login to Launchpad") parser.add_argument(
parser.add_argument('-v', '--verbose', action='count', default=0, "-v", "--verbose", action="count", default=0, help="Increase verbosity/debug"
help="Increase verbosity/debug") )
parser.add_argument('-d', '--download-only', action='store_true', parser.add_argument(
help="Do not extract the source package") "-d", "--download-only", action="store_true", help="Do not extract the source package"
parser.add_argument('-m', '--mirror', action='append', )
help='Preferred mirror(s)') parser.add_argument("-m", "--mirror", action="append", help="Preferred mirror(s)")
parser.add_argument('--no-conf', action='store_true', parser.add_argument(
help="Don't read config files or environment variables") "--no-conf",
parser.add_argument('--no-verify-signature', action='store_true', action="store_true",
help="Don't fail if dsc signature can't be verified") help="Don't read config files or environment variables",
parser.add_argument('-s', '--status', action='append', default=[], )
help="Search for packages with specific status(es)") parser.add_argument(
parser.add_argument('-a', '--arch', default=self._default_arch, "--no-verify-signature",
help=help_default_arch) action="store_true",
parser.add_argument('-p', '--pull', default=self._default_pull, help="Don't fail if dsc signature can't be verified",
help=help_default_pull) )
parser.add_argument('-D', '--distro', default=self._default_distro, parser.add_argument(
help=help_default_distro) "-s",
"--status",
action="append",
default=[],
help="Search for packages with specific status(es)",
)
parser.add_argument("-a", "--arch", default=self._default_arch, help=help_default_arch)
parser.add_argument("-p", "--pull", default=self._default_pull, help=help_default_pull)
parser.add_argument(
"-D", "--distro", default=self._default_distro, help=help_default_distro
)
# add distro-specific params # add distro-specific params
try: try:
@ -163,75 +182,84 @@ class PullPkg(object):
distro = None distro = None
if distro == DISTRO_UBUNTU: if distro == DISTRO_UBUNTU:
parser.add_argument('--security', action='store_true', parser.add_argument(
help='Pull from the Ubuntu Security Team (proposed) PPA') "--security",
parser.add_argument('--upload-queue', action='store_true', action="store_true",
help='Pull from the Ubuntu upload queue') help="Pull from the Ubuntu Security Team (proposed) PPA",
)
parser.add_argument(
"--upload-queue", action="store_true", help="Pull from the Ubuntu upload queue"
)
if distro == DISTRO_PPA: if distro == DISTRO_PPA:
parser.add_argument('--ppa', help='PPA to pull from') parser.add_argument("--ppa", help="PPA to pull from")
if parser.parse_known_args(args)[0].ppa is None: if parser.parse_known_args(args)[0].ppa is None:
# check for any param starting with "ppa:" # check for any param starting with "ppa:"
# if found, move it to a --ppa param # if found, move it to a --ppa param
for param in args: for param in args:
if param.startswith('ppa:'): if param.startswith("ppa:"):
args.remove(param) args.remove(param)
args.insert(0, param) args.insert(0, param)
args.insert(0, '--ppa') args.insert(0, "--ppa")
break break
# add the positional params # add the positional params
parser.add_argument('package', help="Package name to pull") parser.add_argument("package", help="Package name to pull")
parser.add_argument('release', nargs='?', help="Release to pull from") parser.add_argument("release", nargs="?", help="Release to pull from")
parser.add_argument('version', nargs='?', help="Package version to pull") parser.add_argument("version", nargs="?", help="Package version to pull")
epilog = ("Note on --status: if a version is provided, all status types " epilog = (
"will be searched; if no version is provided, by default only " "Note on --status: if a version is provided, all status types "
"'Pending' and 'Published' status will be searched.") "will be searched; if no version is provided, by default only "
"'Pending' and 'Published' status will be searched."
)
# since parser has no --help handler, create a new parser that does # since parser has no --help handler, create a new parser that does
newparser = ArgumentParser(parents=[parser], epilog=epilog) newparser = ArgumentParser(parents=[parser], epilog=epilog)
return self.parse_options(vars(newparser.parse_args(args))) return self.parse_options(vars(newparser.parse_args(args)))
def parse_pull(self, pull): @staticmethod
def parse_pull(pull):
if not pull: if not pull:
raise InvalidPullValueError("Must specify --pull") raise InvalidPullValueError("Must specify --pull")
# allow 'dbgsym' as alias for 'ddebs' # allow 'dbgsym' as alias for 'ddebs'
if pull == 'dbgsym': if pull == "dbgsym":
Logger.debug("Pulling '%s' for '%s'", PULL_DDEBS, pull) Logger.debug("Pulling '%s' for '%s'", PULL_DDEBS, pull)
pull = PULL_DDEBS pull = PULL_DDEBS
# assume anything starting with 'bin' means 'debs' # assume anything starting with 'bin' means 'debs'
if str(pull).startswith('bin'): if str(pull).startswith("bin"):
Logger.debug("Pulling '%s' for '%s'", PULL_DEBS, pull) Logger.debug("Pulling '%s' for '%s'", PULL_DEBS, pull)
pull = PULL_DEBS pull = PULL_DEBS
# verify pull action is valid # verify pull action is valid
if pull not in VALID_PULLS: if pull not in VALID_PULLS:
raise InvalidPullValueError("Invalid pull action '%s'" % pull) raise InvalidPullValueError(f"Invalid pull action '{pull}'")
return pull return pull
def parse_distro(self, distro): @staticmethod
def parse_distro(distro):
if not distro: if not distro:
raise InvalidDistroValueError("Must specify --distro") raise InvalidDistroValueError("Must specify --distro")
distro = distro.lower() distro = distro.lower()
# allow 'lp' for 'ubuntu' # allow 'lp' for 'ubuntu'
if distro == 'lp': if distro == "lp":
Logger.debug("Using distro '%s' for '%s'", DISTRO_UBUNTU, distro) Logger.debug("Using distro '%s' for '%s'", DISTRO_UBUNTU, distro)
distro = DISTRO_UBUNTU distro = DISTRO_UBUNTU
# assume anything with 'cloud' is UCA # assume anything with 'cloud' is UCA
if re.match(r'.*cloud.*', distro): if re.match(r".*cloud.*", distro):
Logger.debug("Using distro '%s' for '%s'", DISTRO_UCA, distro) Logger.debug("Using distro '%s' for '%s'", DISTRO_UCA, distro)
distro = DISTRO_UCA distro = DISTRO_UCA
# verify distro is valid # verify distro is valid
if distro not in VALID_DISTROS: if distro not in VALID_DISTROS:
raise InvalidDistroValueError("Invalid distro '%s'" % distro) raise InvalidDistroValueError(f"Invalid distro '{distro}'")
return distro return distro
def parse_release(self, distro, release): @staticmethod
def parse_release(distro, release):
if distro == DISTRO_UCA: if distro == DISTRO_UCA:
return UbuntuCloudArchiveSourcePackage.parseReleaseAndPocket(release) return UbuntuCloudArchiveSourcePackage.parseReleaseAndPocket(release)
@ -249,15 +277,14 @@ class PullPkg(object):
if distro == DISTRO_PPA: if distro == DISTRO_PPA:
# PPAs are part of Ubuntu distribution # PPAs are part of Ubuntu distribution
d = Distribution(DISTRO_UBUNTU) distribution = Distribution(DISTRO_UBUNTU)
else: else:
d = Distribution(distro) distribution = Distribution(distro)
# let SeriesNotFoundException flow up # let SeriesNotFoundException flow up
d.getSeries(release) distribution.getSeries(release)
Logger.debug("Using distro '%s' release '%s' pocket '%s'", Logger.debug("Using distro '%s' release '%s' pocket '%s'", distro, release, pocket)
distro, release, pocket)
return (release, pocket) return (release, pocket)
def parse_release_and_version(self, distro, release, version, try_swap=True): def parse_release_and_version(self, distro, release, version, try_swap=True):
@ -281,153 +308,196 @@ class PullPkg(object):
# they should all be provided, though the optional ones may be None # they should all be provided, though the optional ones may be None
# type bool # type bool
assert 'verbose' in options assert "verbose" in options
assert 'download_only' in options assert "download_only" in options
assert 'no_conf' in options assert "no_conf" in options
assert 'no_verify_signature' in options assert "no_verify_signature" in options
assert 'status' in options assert "status" in options
# type string # type string
assert 'pull' in options assert "pull" in options
assert 'distro' in options assert "distro" in options
assert 'arch' in options assert "arch" in options
assert 'package' in options assert "package" in options
# type string, optional # type string, optional
assert 'release' in options assert "release" in options
assert 'version' in options assert "version" in options
# type list of strings, optional # type list of strings, optional
assert 'mirror' in options assert "mirror" in options
options['pull'] = self.parse_pull(options['pull']) options["pull"] = self.parse_pull(options["pull"])
options['distro'] = self.parse_distro(options['distro']) options["distro"] = self.parse_distro(options["distro"])
# ensure these are always included so we can just check for None/False later # ensure these are always included so we can just check for None/False later
options['ppa'] = options.get('ppa', None) options["ppa"] = options.get("ppa", None)
options['security'] = options.get('security', False) options["security"] = options.get("security", False)
options['upload_queue'] = options.get('upload_queue', False) options["upload_queue"] = options.get("upload_queue", False)
return options return options
def _get_params(self, options): def _get_params(self, options):
distro = options['distro'] distro = options["distro"]
pull = options['pull'] pull = options["pull"]
params = {} params = {}
params['package'] = options['package'] params["package"] = options["package"]
params["arch"] = options["arch"]
if options['release']: if options["release"]:
(r, v, p) = self.parse_release_and_version(distro, options['release'], (release, version, pocket) = self.parse_release_and_version(
options['version']) distro, options["release"], options["version"]
params['series'] = r )
params['version'] = v params["series"] = release
params['pocket'] = p params["version"] = version
params["pocket"] = pocket
if (params['package'].endswith('.dsc') and not params['series'] and not params['version']): if params["package"].endswith(".dsc") and not params["series"] and not params["version"]:
params['dscfile'] = params['package'] params["dscfile"] = params["package"]
params.pop('package') params.pop("package")
if options['security']: if options["security"]:
if options['ppa']: if options["ppa"]:
Logger.warning('Both --security and --ppa specified, ignoring --ppa') Logger.warning("Both --security and --ppa specified, ignoring --ppa")
Logger.debug('Checking Ubuntu Security PPA') Logger.debug("Checking Ubuntu Security PPA")
# --security is just a shortcut for --ppa ppa:ubuntu-security-proposed/ppa # --security is just a shortcut for --ppa ppa:ubuntu-security-proposed/ppa
options['ppa'] = 'ubuntu-security-proposed/ppa' options["ppa"] = "ubuntu-security-proposed/ppa"
if options['ppa']: if options["ppa"]:
if options['ppa'].startswith('ppa:'): if options["ppa"].startswith("ppa:"):
params['ppa'] = options['ppa'][4:] params["ppa"] = options["ppa"][4:]
else: else:
params['ppa'] = options['ppa'] params["ppa"] = options["ppa"]
elif distro == DISTRO_PPA: elif distro == DISTRO_PPA:
raise ValueError('Must specify PPA to pull from') raise ValueError("Must specify PPA to pull from")
mirrors = [] mirrors = []
if options['mirror']: if options["mirror"]:
mirrors.extend(options['mirror']) mirrors.extend(options["mirror"])
if pull == PULL_DDEBS: if pull == PULL_DDEBS:
config = UDTConfig(options['no_conf']) config = UDTConfig(options["no_conf"])
ddebs_mirror = config.get_value(distro.upper() + '_DDEBS_MIRROR') ddebs_mirror = config.get_value(distro.upper() + "_DDEBS_MIRROR")
if ddebs_mirror: if ddebs_mirror:
mirrors.append(ddebs_mirror) mirrors.append(ddebs_mirror)
if mirrors: if mirrors:
Logger.debug("using mirrors %s", ", ".join(mirrors)) Logger.debug("using mirrors %s", ", ".join(mirrors))
params['mirrors'] = mirrors params["mirrors"] = mirrors
params['verify_signature'] = not options['no_verify_signature'] params["verify_signature"] = not options["no_verify_signature"]
params['status'] = STATUSES if 'all' in options['status'] else options['status'] params["status"] = STATUSES if "all" in options["status"] else options["status"]
# special handling for upload queue # special handling for upload queue
if options['upload_queue']: if options["upload_queue"]:
if len(options['status']) > 1: if len(options["status"]) > 1:
raise ValueError("Too many --status provided, " raise ValueError(
"can only search for a single status or 'all'") "Too many --status provided, can only search for a single status or 'all'"
if not options['status']: )
params['status'] = None if not options["status"]:
elif options['status'][0].lower() == 'all': params["status"] = None
params['status'] = 'all' elif options["status"][0].lower() == "all":
elif options['status'][0].capitalize() in UPLOAD_QUEUE_STATUSES: params["status"] = "all"
params['status'] = options['status'][0].capitalize() elif options["status"][0].capitalize() in UPLOAD_QUEUE_STATUSES:
params["status"] = options["status"][0].capitalize()
else: else:
msg = ("Invalid upload queue status '%s': valid values are %s" % raise ValueError(
(options['status'][0], ', '.join(UPLOAD_QUEUE_STATUSES))) f"Invalid upload queue status '{options['status'][0]}':"
raise ValueError(msg) f" valid values are {', '.join(UPLOAD_QUEUE_STATUSES)}"
)
return params return params
def pull(self, args=sys.argv[1:]): def pull(self, args=None):
"""Pull (download) specified package file(s)""" """Pull (download) specified package file(s)"""
options = self.parse_args(args) options = self.parse_args(args)
if options['verbose']: if options["verbose"]:
Logger.setLevel(logging.DEBUG) Logger.setLevel(logging.DEBUG)
if options['verbose'] > 1: if options["verbose"] > 1:
logging.getLogger(__package__).setLevel(logging.DEBUG) logging.getLogger(__package__).setLevel(logging.DEBUG)
Logger.debug("pullpkg options: %s", options) Logger.debug("pullpkg options: %s", options)
pull = options['pull'] pull = options["pull"]
distro = options['distro'] distro = options["distro"]
if options['login']: if options["login"]:
Logger.debug("Logging in to Launchpad:") Logger.debug("Logging in to Launchpad:")
try: try:
Launchpad.login() Launchpad.login()
except AlreadyLoggedInError: except AlreadyLoggedInError:
Logger.error("Launchpad singleton has already performed a login, " Logger.error(
"and its design prevents another login") "Launchpad singleton has already performed a login, "
"and its design prevents another login"
)
Logger.warning("Continuing anyway, with existing Launchpad instance") Logger.warning("Continuing anyway, with existing Launchpad instance")
params = self._get_params(options) params = self._get_params(options)
package = params['package'] package = params["package"]
if options['upload_queue']: if options["upload_queue"]:
# upload queue API is different/simpler # upload queue API is different/simpler
self.pull_upload_queue(pull, arch=options['arch'], self.pull_upload_queue( # pylint: disable=missing-kwoa
download_only=options['download_only'], pull, arch=options["arch"], download_only=options["download_only"], **params
**params) )
return return
# call implementation, and allow exceptions to flow up to caller # call implementation, and allow exceptions to flow up to caller
srcpkg = DISTRO_PKG_CLASS[distro](**params) srcpkg = DISTRO_PKG_CLASS[distro](**params)
spph = srcpkg.lp_spph spph = srcpkg.lp_spph
Logger.info('Found %s', spph.display_name) Logger.info("Found %s", spph.display_name)
# The VCS detection logic was modeled after `apt source`
for key in srcpkg.dsc.keys():
original_key = key
key = key.lower()
if key.startswith("vcs-"):
if key == "vcs-browser":
continue
if key == "vcs-git":
vcs = "Git"
elif key == "vcs-bzr":
vcs = "Bazaar"
else:
continue
uri = srcpkg.dsc[original_key]
Logger.warning(
"\nNOTICE: '%s' packaging is maintained in "
"the '%s' version control system at:\n %s\n",
package,
vcs,
uri,
)
if vcs == "Bazaar":
vcscmd = " $ bzr branch " + uri
elif vcs == "Git":
vcscmd = " $ git clone " + uri
if vcscmd:
Logger.info(
"Please use:\n%s\n"
"to retrieve the latest (possibly unreleased) updates to the package.\n",
vcscmd,
)
if pull == PULL_LIST: if pull == PULL_LIST:
Logger.info("Source files:") Logger.info("Source files:")
for f in srcpkg.dsc['Files']: for f in srcpkg.dsc["Files"]:
Logger.info(" %s", f['name']) Logger.info(" %s", f["name"])
Logger.info("Binary files:") Logger.info("Binary files:")
for f in spph.getBinaries(options['arch']): for f in spph.getBinaries(options["arch"]):
archtext = '' archtext = ""
name = f.getFileName() name = f.getFileName()
if name.rpartition('.')[0].endswith('all'): if name.rpartition(".")[0].endswith("all"):
archtext = f" ({f.arch})" archtext = f" ({f.arch})"
Logger.info(f" {name}{archtext}") Logger.info(" %s%s", name, archtext)
elif pull == PULL_SOURCE: elif pull == PULL_SOURCE:
# allow DownloadError to flow up to caller # allow DownloadError to flow up to caller
srcpkg.pull() srcpkg.pull()
if options['download_only']: if options["download_only"]:
Logger.debug("--download-only specified, not extracting") Logger.debug("--download-only specified, not extracting")
else: else:
srcpkg.unpack() srcpkg.unpack()
@ -435,104 +505,116 @@ class PullPkg(object):
name = None name = None
if package != spph.getPackageName(): if package != spph.getPackageName():
Logger.info("Pulling only binary package '%s'", package) Logger.info("Pulling only binary package '%s'", package)
Logger.info("Use package name '%s' to pull all binary packages", Logger.info(
spph.getPackageName()) "Use package name '%s' to pull all binary packages", spph.getPackageName()
)
name = package name = package
# e.g. 'debs' -> 'deb' # e.g. 'debs' -> 'deb'
ext = pull.rstrip('s') ext = pull.rstrip("s")
if distro == DISTRO_DEBIAN: if distro == DISTRO_DEBIAN:
# Debian ddebs don't use .ddeb extension, unfortunately :( # Debian ddebs don't use .ddeb extension, unfortunately :(
if pull in [PULL_DEBS, PULL_DDEBS]: if pull in [PULL_DEBS, PULL_DDEBS]:
name = name or '.*' name = name or ".*"
ext = 'deb' ext = "deb"
if pull == PULL_DEBS: if pull == PULL_DEBS:
name += r'(?<!-dbgsym)$' name += r"(?<!-dbgsym)$"
if pull == PULL_DDEBS: if pull == PULL_DDEBS:
name += r'-dbgsym$' name += r"-dbgsym$"
# allow DownloadError to flow up to caller # allow DownloadError to flow up to caller
total = srcpkg.pull_binaries(name=name, ext=ext, arch=options['arch']) total = srcpkg.pull_binaries(name=name, ext=ext, arch=options["arch"])
if total < 1: if total < 1:
Logger.error("No %s found for %s %s", pull, Logger.error("No %s found for %s %s", pull, package, spph.getVersion())
package, spph.getVersion())
else: else:
Logger.error("Internal error: invalid pull value after parse_pull()") Logger.error("Internal error: invalid pull value after parse_pull()")
raise InvalidPullValueError("Invalid pull value '%s'" % pull) raise InvalidPullValueError(f"Invalid pull value '{pull}'")
def pull_upload_queue(self, pull, *, def pull_upload_queue(
package, version=None, arch=None, series=None, pocket=None, self,
status=None, download_only=None, **kwargs): pull,
*,
package,
version=None,
arch=None,
series=None,
pocket=None,
status=None,
download_only=None,
**kwargs,
): # pylint: disable=no-self-use,unused-argument
if not series: if not series:
Logger.error("Using --upload-queue requires specifying series") Logger.error("Using --upload-queue requires specifying series")
return return
series = Distribution('ubuntu').getSeries(series) series = Distribution("ubuntu").getSeries(series)
queueparams = {'name': package} queueparams = {"name": package}
if pocket: if pocket:
queueparams['pocket'] = pocket queueparams["pocket"] = pocket
if status == 'all': if status == "all":
queueparams['status'] = None queueparams["status"] = None
queuetype = 'any' queuetype = "any"
elif status: elif status:
queueparams['status'] = status queueparams["status"] = status
queuetype = status queuetype = status
else: else:
queuetype = 'Unapproved' queuetype = "Unapproved"
packages = [p for p in series.getPackageUploads(**queueparams) if packages = [
p.package_version == version or p
str(p.id) == version or for p in series.getPackageUploads(**queueparams)
not version] if p.package_version == version or str(p.id) == version or not version
]
if pull == PULL_SOURCE: if pull == PULL_SOURCE:
packages = [p for p in packages if p.contains_source] packages = [p for p in packages if p.contains_source]
elif pull in VALID_BINARY_PULLS: elif pull in VALID_BINARY_PULLS:
packages = [p for p in packages if packages = [
p.contains_build and p
(arch in ['all', 'any'] or for p in packages
arch in p.display_arches.replace(',', '').split())] if p.contains_build
and (arch in ["all", "any"] or arch in p.display_arches.replace(",", "").split())
]
if not packages: if not packages:
msg = ("Package %s not found in %s upload queue for %s" % msg = f"Package {package} not found in {queuetype} upload queue for {series.name}"
(package, queuetype, series.name))
if version: if version:
msg += " with version/id %s" % version msg += f" with version/id {version}"
if pull in VALID_BINARY_PULLS: if pull in VALID_BINARY_PULLS:
msg += " for arch %s" % arch msg += f" for arch {arch}"
raise PackageNotFoundException(msg) raise PackageNotFoundException(msg)
if pull == PULL_LIST: if pull == PULL_LIST:
for p in packages: for pkg in packages:
msg = "Found %s %s (ID %s)" % (p.package_name, p.package_version, p.id) msg = f"Found {pkg.package_name} {pkg.package_version} (ID {pkg.id})"
if p.display_arches: if pkg.display_arches:
msg += " arch %s" % p.display_arches msg += f" arch {pkg.display_arches}"
Logger.info(msg) Logger.info(msg)
url = p.changesFileUrl() url = pkg.changesFileUrl()
if url: if url:
Logger.info("Changes file:") Logger.info("Changes file:")
Logger.info(" %s", url) Logger.info(" %s", url)
else: else:
Logger.info("No changes file") Logger.info("No changes file")
urls = p.sourceFileUrls() urls = pkg.sourceFileUrls()
if urls: if urls:
Logger.info("Source files:") Logger.info("Source files:")
for url in urls: for url in urls:
Logger.info(" %s", url) Logger.info(" %s", url)
else: else:
Logger.info("No source files") Logger.info("No source files")
urls = p.binaryFileUrls() urls = pkg.binaryFileUrls()
if urls: if urls:
Logger.info("Binary files:") Logger.info("Binary files:")
for url in urls: for url in urls:
Logger.info(" %s", url) Logger.info(" %s", url)
Logger.info(" { %s }" % p.binaryFileProperties(url)) Logger.info(" { %s }", pkg.binaryFileProperties(url))
else: else:
Logger.info("No binary files") Logger.info("No binary files")
urls = p.customFileUrls() urls = pkg.customFileUrls()
if urls: if urls:
Logger.info("Custom files:") Logger.info("Custom files:")
for url in urls: for url in urls:
@ -542,53 +624,58 @@ class PullPkg(object):
if len(packages) > 1: if len(packages) > 1:
msg = "Found multiple packages" msg = "Found multiple packages"
if version: if version:
msg += " with version %s, please specify the ID instead" % version msg += f" with version {version}, please specify the ID instead"
else: else:
msg += ", please specify the version" msg += ", please specify the version"
Logger.error("Available package versions/ids are:") Logger.error("Available package versions/ids are:")
for p in packages: for pkg in packages:
Logger.error("%s %s (id %s)" % (p.package_name, p.package_version, p.id)) Logger.error("%s %s (id %s)", pkg.package_name, pkg.package_version, pkg.id)
raise PackageNotFoundException(msg) raise PackageNotFoundException(msg)
p = packages[0] pkg = packages[0]
urls = set(p.customFileUrls()) urls = set(pkg.customFileUrls())
if p.changesFileUrl(): if pkg.changesFileUrl():
urls.add(p.changesFileUrl()) urls.add(pkg.changesFileUrl())
if pull == PULL_SOURCE: if pull == PULL_SOURCE:
urls |= set(p.sourceFileUrls()) urls |= set(pkg.sourceFileUrls())
if not urls: if not urls:
Logger.error("No source files to download") Logger.error("No source files to download")
dscfile = None dscfile = None
for url in urls: for url in urls:
dst = download(url, os.getcwd()) dst = download(url, os.getcwd())
if dst.name.endswith('.dsc'): if dst.name.endswith(".dsc"):
dscfile = dst dscfile = dst
if download_only: if download_only:
Logger.debug("--download-only specified, not extracting") Logger.debug("--download-only specified, not extracting")
elif not dscfile: elif not dscfile:
Logger.error("No source dsc file found, cannot extract") Logger.error("No source dsc file found, cannot extract")
else: else:
cmd = ['dpkg-source', '-x', dscfile.name] cmd = ["dpkg-source", "-x", dscfile.name]
Logger.debug(' '.join(cmd)) Logger.debug(" ".join(cmd))
result = subprocess.run(cmd, encoding='utf-8', result = subprocess.run(
stdout=subprocess.PIPE, stderr=subprocess.STDOUT) cmd,
check=False,
encoding="utf-8",
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
if result.returncode != 0: if result.returncode != 0:
Logger.error('Source unpack failed.') Logger.error("Source unpack failed.")
Logger.debug(result.stdout) Logger.debug(result.stdout)
else: else:
name = '.*' name = ".*"
if pull == PULL_DEBS: if pull == PULL_DEBS:
name = r'{}(?<!-di)(?<!-dbgsym)$'.format(name) name = rf"{name}(?<!-di)(?<!-dbgsym)$"
elif pull == PULL_DDEBS: elif pull == PULL_DDEBS:
name += '-dbgsym$' name += "-dbgsym$"
elif pull == PULL_UDEBS: elif pull == PULL_UDEBS:
name += '-di$' name += "-di$"
else: else:
raise InvalidPullValueError("Invalid pull value %s" % pull) raise InvalidPullValueError(f"Invalid pull value {pull}")
urls |= set(p.binaryFileUrls()) urls |= set(pkg.binaryFileUrls())
if not urls: if not urls:
Logger.error("No binary files to download") Logger.error("No binary files to download")
for url in urls: for url in urls:

View File

@ -16,14 +16,14 @@
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
import tempfile
import os import os
import re import re
import subprocess import subprocess
import sys import sys
import tempfile
class Question(object): class Question:
def __init__(self, options, show_help=True): def __init__(self, options, show_help=True):
assert len(options) >= 2 assert len(options) >= 2
self.options = [s.lower() for s in options] self.options = [s.lower() for s in options]
@ -31,9 +31,9 @@ class Question(object):
def get_options(self): def get_options(self):
if len(self.options) == 2: if len(self.options) == 2:
options = self.options[0] + " or " + self.options[1] options = f"{self.options[0]} or {self.options[1]}"
else: else:
options = ", ".join(self.options[:-1]) + ", or " + self.options[-1] options = f"{', '.join(self.options[:-1])}, or {self.options[-1]}"
return options return options
def ask(self, question, default=None): def ask(self, question, default=None):
@ -57,7 +57,7 @@ class Question(object):
try: try:
selected = input(question).strip().lower() selected = input(question).strip().lower()
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
print('\nAborting as requested.') print("\nAborting as requested.")
sys.exit(1) sys.exit(1)
if selected == "": if selected == "":
selected = default selected = default
@ -67,7 +67,7 @@ class Question(object):
if selected == option[0]: if selected == option[0]:
selected = option selected = option
if selected not in self.options: if selected not in self.options:
print("Please answer the question with " + self.get_options() + ".") print(f"Please answer the question with {self.get_options()}.")
return selected return selected
@ -78,7 +78,7 @@ class YesNoQuestion(Question):
def input_number(question, min_number, max_number, default=None): def input_number(question, min_number, max_number, default=None):
if default: if default:
question += " [%i]? " % (default) question += f" [{default}]? "
else: else:
question += "? " question += "? "
selected = None selected = None
@ -86,7 +86,7 @@ def input_number(question, min_number, max_number, default=None):
try: try:
selected = input(question).strip() selected = input(question).strip()
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
print('\nAborting as requested.') print("\nAborting as requested.")
sys.exit(1) sys.exit(1)
if default and selected == "": if default and selected == "":
selected = default selected = default
@ -94,40 +94,40 @@ def input_number(question, min_number, max_number, default=None):
try: try:
selected = int(selected) selected = int(selected)
if selected < min_number or selected > max_number: if selected < min_number or selected > max_number:
print("Please input a number between %i and %i." % (min_number, max_number)) print(f"Please input a number between {min_number} and {max_number}.")
except ValueError: except ValueError:
print("Please input a number.") print("Please input a number.")
assert type(selected) == int assert isinstance(selected, int)
return selected return selected
def confirmation_prompt(message=None, action=None): def confirmation_prompt(message=None, action=None):
'''Display message, or a stock message including action, and wait for the """Display message, or a stock message including action, and wait for the
user to press Enter user to press Enter
''' """
if message is None: if message is None:
if action is None: if action is None:
action = 'continue' action = "continue"
message = 'Press [Enter] to %s. Press [Ctrl-C] to abort now.' % action message = f"Press [Enter] to {action}. Press [Ctrl-C] to abort now."
try: try:
input(message) input(message)
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
print('\nAborting as requested.') print("\nAborting as requested.")
sys.exit(1) sys.exit(1)
class EditFile(object): class EditFile:
def __init__(self, filename, description, placeholders=None): def __init__(self, filename, description, placeholders=None):
self.filename = filename self.filename = filename
self.description = description self.description = description
if placeholders is None: if placeholders is None:
placeholders = (re.compile(r'^>>>.*<<<$', re.UNICODE),) placeholders = (re.compile(r"^>>>.*<<<$", re.UNICODE),)
self.placeholders = placeholders self.placeholders = placeholders
def edit(self, optional=False): def edit(self, optional=False):
if optional: if optional:
print("\n\nCurrently the %s looks like:" % self.description) print(f"\n\nCurrently the {self.description} looks like:")
with open(self.filename, 'r', encoding='utf-8') as f: with open(self.filename, "r", encoding="utf-8") as f:
print(f.read()) print(f.read())
if YesNoQuestion().ask("Edit", "no") == "no": if YesNoQuestion().ask("Edit", "no") == "no":
return return
@ -135,68 +135,65 @@ class EditFile(object):
done = False done = False
while not done: while not done:
old_mtime = os.stat(self.filename).st_mtime old_mtime = os.stat(self.filename).st_mtime
subprocess.check_call(['sensible-editor', self.filename]) subprocess.check_call(["sensible-editor", self.filename])
modified = old_mtime != os.stat(self.filename).st_mtime modified = old_mtime != os.stat(self.filename).st_mtime
placeholders_present = False placeholders_present = False
if self.placeholders: if self.placeholders:
with open(self.filename, 'r', encoding='utf-8') as f: with open(self.filename, "r", encoding="utf-8") as f:
for line in f: for line in f:
for placeholder in self.placeholders: for placeholder in self.placeholders:
if placeholder.search(line.strip()): if placeholder.search(line.strip()):
placeholders_present = True placeholders_present = True
if placeholders_present: if placeholders_present:
print("Placeholders still present in the %s. " print(
"Please replace them with useful information." f"Placeholders still present in the {self.description}. "
% self.description) f"Please replace them with useful information."
confirmation_prompt(action='edit again') )
confirmation_prompt(action="edit again")
elif not modified: elif not modified:
print("The %s was not modified" % self.description) print(f"The {self.description} was not modified")
if YesNoQuestion().ask("Edit again", "yes") == "no": if YesNoQuestion().ask("Edit again", "yes") == "no":
done = True done = True
elif self.check_edit(): elif self.check_edit():
done = True done = True
def check_edit(self): def check_edit(self): # pylint: disable=no-self-use
'''Override this to implement extra checks on the edited report. """Override this to implement extra checks on the edited report.
Should return False if another round of editing is needed, Should return False if another round of editing is needed,
and should prompt the user to confirm that, if necessary. and should prompt the user to confirm that, if necessary.
''' """
return True return True
class EditBugReport(EditFile): class EditBugReport(EditFile):
split_re = re.compile(r'^Summary.*?:\s+(.*?)\s+' split_re = re.compile(r"^Summary.*?:\s+(.*?)\s+Description:\s+(.*)$", re.DOTALL | re.UNICODE)
r'Description:\s+(.*)$',
re.DOTALL | re.UNICODE)
def __init__(self, subject, body, placeholders=None): def __init__(self, subject, body, placeholders=None):
prefix = os.path.basename(sys.argv[0]) + '_' prefix = f"{os.path.basename(sys.argv[0])}_"
tmpfile = tempfile.NamedTemporaryFile(prefix=prefix, suffix='.txt', tmpfile = tempfile.NamedTemporaryFile(prefix=prefix, suffix=".txt", delete=False)
delete=False) tmpfile.write((f"Summary (one line):\n{subject}\n\nDescription:\n{body}").encode("utf-8"))
tmpfile.write((u'Summary (one line):\n%s\n\nDescription:\n%s'
% (subject, body)).encode('utf-8'))
tmpfile.close() tmpfile.close()
super(EditBugReport, self).__init__(tmpfile.name, 'bug report', super().__init__(tmpfile.name, "bug report", placeholders)
placeholders)
def check_edit(self): def check_edit(self):
with open(self.filename, 'r', encoding='utf-8') as f: with open(self.filename, "r", encoding="utf-8") as f:
report = f.read() report = f.read()
if self.split_re.match(report) is None: if self.split_re.match(report) is None:
print("The %s doesn't start with 'Summary:' and 'Description:' " print(
"blocks" % self.description) f"The {self.description} doesn't start with 'Summary:' and 'Description:' blocks"
confirmation_prompt('edit again') )
confirmation_prompt("edit again")
return False return False
return True return True
def get_report(self): def get_report(self):
with open(self.filename, 'r', encoding='utf-8') as f: with open(self.filename, "r", encoding="utf-8") as f:
report = f.read() report = f.read()
match = self.split_re.match(report) match = self.split_re.match(report)
title = match.group(1).replace(u'\n', u' ') title = match.group(1).replace("\n", " ")
report = (title, match.group(2)) report = (title, match.group(2))
os.unlink(self.filename) os.unlink(self.filename)
return report return report

View File

@ -22,13 +22,12 @@ class RDependsException(Exception):
pass pass
def query_rdepends(package, release, arch, def query_rdepends(package, release, arch, server="http://qa.ubuntuwire.org/rdepends"):
server='http://qa.ubuntuwire.org/rdepends'):
"""Look up a packages reverse-dependencies on the Ubuntuwire """Look up a packages reverse-dependencies on the Ubuntuwire
Reverse- webservice Reverse- webservice
""" """
url = os.path.join(server, 'v1', release, arch, package) url = os.path.join(server, "v1", release, arch, package)
response, data = httplib2.Http().request(url) response, data = httplib2.Http().request(url)
if response.status != 200: if response.status != 200:

View File

@ -20,6 +20,7 @@
# Please see the /usr/share/common-licenses/GPL-2 file for the full text # Please see the /usr/share/common-licenses/GPL-2 file for the full text
# of the GNU General Public License license. # of the GNU General Public License license.
import logging
import re import re
from debian.deb822 import Changes from debian.deb822 import Changes
@ -27,16 +28,19 @@ from distro_info import DebianDistroInfo, DistroDataOutdated
from httplib2 import Http, HttpLib2Error from httplib2 import Http, HttpLib2Error
from ubuntutools.lp import udtexceptions from ubuntutools.lp import udtexceptions
from ubuntutools.lp.lpapicache import (Launchpad, Distribution, PersonTeam, from ubuntutools.lp.lpapicache import (
DistributionSourcePackage) Distribution,
DistributionSourcePackage,
Launchpad,
PersonTeam,
)
from ubuntutools.question import confirmation_prompt from ubuntutools.question import confirmation_prompt
import logging
Logger = logging.getLogger(__name__) Logger = logging.getLogger(__name__)
def get_debian_srcpkg(name, release): def get_debian_srcpkg(name, release):
debian = Distribution('debian') debian = Distribution("debian")
debian_archive = debian.getArchive() debian_archive = debian.getArchive()
try: try:
@ -47,82 +51,83 @@ def get_debian_srcpkg(name, release):
return debian_archive.getSourcePackage(name, release) return debian_archive.getSourcePackage(name, release)
def get_ubuntu_srcpkg(name, release, pocket='Release'): def get_ubuntu_srcpkg(name, release, pocket="Release"):
ubuntu = Distribution('ubuntu') ubuntu = Distribution("ubuntu")
ubuntu_archive = ubuntu.getArchive() ubuntu_archive = ubuntu.getArchive()
try: try:
return ubuntu_archive.getSourcePackage(name, release, pocket) return ubuntu_archive.getSourcePackage(name, release, pocket)
except udtexceptions.PackageNotFoundException: except udtexceptions.PackageNotFoundException:
if pocket != 'Release': if pocket != "Release":
parent_pocket = 'Release' parent_pocket = "Release"
if pocket == 'Updates': if pocket == "Updates":
parent_pocket = 'Proposed' parent_pocket = "Proposed"
return get_ubuntu_srcpkg(name, release, parent_pocket) return get_ubuntu_srcpkg(name, release, parent_pocket)
raise raise
def need_sponsorship(name, component, release): def need_sponsorship(name, component, release):
''' """
Check if the user has upload permissions for either the package Check if the user has upload permissions for either the package
itself or the component itself or the component
''' """
archive = Distribution('ubuntu').getArchive() archive = Distribution("ubuntu").getArchive()
distroseries = Distribution('ubuntu').getSeries(release) distroseries = Distribution("ubuntu").getSeries(release)
need_sponsor = not PersonTeam.me.canUploadPackage(archive, distroseries, need_sponsor = not PersonTeam.me.canUploadPackage(archive, distroseries, name, component)
name, component)
if need_sponsor: if need_sponsor:
print('''You are not able to upload this package directly to Ubuntu. print(
"""You are not able to upload this package directly to Ubuntu.
Your sync request shall require an approval by a member of the appropriate Your sync request shall require an approval by a member of the appropriate
sponsorship team, who shall be subscribed to this bug report. sponsorship team, who shall be subscribed to this bug report.
This must be done before it can be processed by a member of the Ubuntu Archive This must be done before it can be processed by a member of the Ubuntu Archive
team.''') team."""
)
confirmation_prompt() confirmation_prompt()
return need_sponsor return need_sponsor
def check_existing_reports(srcpkg): def check_existing_reports(srcpkg):
''' """
Check existing bug reports on Launchpad for a possible sync request. Check existing bug reports on Launchpad for a possible sync request.
If found ask for confirmation on filing a request. If found ask for confirmation on filing a request.
''' """
# Fetch the package's bug list from Launchpad # Fetch the package's bug list from Launchpad
pkg = Distribution('ubuntu').getSourcePackage(name=srcpkg) pkg = Distribution("ubuntu").getSourcePackage(name=srcpkg)
pkg_bug_list = pkg.searchTasks(status=["Incomplete", "New", "Confirmed", pkg_bug_list = pkg.searchTasks(
"Triaged", "In Progress", status=["Incomplete", "New", "Confirmed", "Triaged", "In Progress", "Fix Committed"],
"Fix Committed"], omit_duplicates=True,
omit_duplicates=True) )
# Search bug list for other sync requests. # Search bug list for other sync requests.
for bug in pkg_bug_list: for bug in pkg_bug_list:
# check for Sync or sync and the package name # check for Sync or sync and the package name
if not bug.is_complete and 'ync %s' % srcpkg in bug.title: if not bug.is_complete and f"ync {srcpkg}" in bug.title:
print('The following bug could be a possible duplicate sync bug ' print(
'on Launchpad:\n' f"The following bug could be a possible duplicate sync bug on Launchpad:\n"
' * %s (%s)\n' f" * {bug.title} ({bug.web_link})\n"
'Please check the above URL to verify this before ' f"Please check the above URL to verify this before continuing."
'continuing.' )
% (bug.title, bug.web_link))
confirmation_prompt() confirmation_prompt()
def get_ubuntu_delta_changelog(srcpkg): def get_ubuntu_delta_changelog(srcpkg):
''' """
Download the Ubuntu changelog and extract the entries since the last sync Download the Ubuntu changelog and extract the entries since the last sync
from Debian. from Debian.
''' """
archive = Distribution('ubuntu').getArchive() archive = Distribution("ubuntu").getArchive()
spph = archive.getPublishedSources(source_name=srcpkg.getPackageName(), spph = archive.getPublishedSources(
exact_match=True, pocket='Release') source_name=srcpkg.getPackageName(), exact_match=True, pocket="Release"
)
debian_info = DebianDistroInfo() debian_info = DebianDistroInfo()
topline = re.compile(r'^(\w%(name_chars)s*) \(([^\(\) \t]+)\)' name_chars = "[-+0-9a-z.]"
r'((\s+%(name_chars)s+)+)\;' topline = re.compile(
% {'name_chars': '[-+0-9a-z.]'}, rf"^(\w%({name_chars})s*) \(([^\(\) \t]+)\)((\s+%({name_chars})s+)+)\;", re.IGNORECASE
re.IGNORECASE) )
delta = [] delta = []
for record in spph: for record in spph:
changes_url = record.changesFileUrl() changes_url = record.changesFileUrl()
@ -130,61 +135,57 @@ def get_ubuntu_delta_changelog(srcpkg):
# Native sync # Native sync
break break
try: try:
response, body = Http().request(changes_url) response = Http().request(changes_url)[0]
except HttpLib2Error as e: except HttpLib2Error as e:
Logger.error(str(e)) Logger.error(str(e))
break break
if response.status != 200: if response.status != 200:
Logger.error("%s: %s %s", changes_url, response.status, Logger.error("%s: %s %s", changes_url, response.status, response.reason)
response.reason)
break break
changes = Changes(Http().request(changes_url)[1]) changes = Changes(Http().request(changes_url)[1])
for line in changes['Changes'].splitlines(): for line in changes["Changes"].splitlines():
line = line[1:] line = line[1:]
m = topline.match(line) match = topline.match(line)
if m: if match:
distribution = m.group(3).split()[0].split('-')[0] distribution = match.group(3).split()[0].split("-")[0]
if debian_info.valid(distribution): if debian_info.valid(distribution):
break break
if line.startswith(u' '): if line.startswith(" "):
delta.append(line) delta.append(line)
else: else:
continue continue
break break
return '\n'.join(delta) return "\n".join(delta)
def post_bug(srcpkg, subscribe, status, bugtitle, bugtext): def post_bug(srcpkg, subscribe, status, bugtitle, bugtext):
''' """
Use the LP API to file the sync request. Use the LP API to file the sync request.
''' """
print('The final report is:\nSummary: %s\nDescription:\n%s\n' print(f"The final report is:\nSummary: {bugtitle}\nDescription:\n{bugtext}\n")
% (bugtitle, bugtext))
confirmation_prompt() confirmation_prompt()
if srcpkg: if srcpkg:
bug_target = DistributionSourcePackage( # pylint: disable=protected-access
'%subuntu/+source/%s' % (Launchpad._root_uri, srcpkg)) bug_target = DistributionSourcePackage(f"{Launchpad._root_uri}ubuntu/+source/{srcpkg}")
else: else:
# new source package # new source package
bug_target = Distribution('ubuntu') bug_target = Distribution("ubuntu")
# create bug # create bug
bug = Launchpad.bugs.createBug(title=bugtitle, description=bugtext, bug = Launchpad.bugs.createBug(title=bugtitle, description=bugtext, target=bug_target())
target=bug_target())
# newly created bugreports have only one task # newly created bugreports have only one task
task = bug.bug_tasks[0] task = bug.bug_tasks[0]
# only members of ubuntu-bugcontrol can set importance # only members of ubuntu-bugcontrol can set importance
if PersonTeam.me.isLpTeamMember('ubuntu-bugcontrol'): if PersonTeam.me.isLpTeamMember("ubuntu-bugcontrol"):
task.importance = 'Wishlist' task.importance = "Wishlist"
task.status = status task.status = status
task.lp_save() task.lp_save()
bug.subscribe(person=PersonTeam(subscribe)()) bug.subscribe(person=PersonTeam(subscribe)())
print('Sync request filed as bug #%i: %s' print(f"Sync request filed as bug #{bug.id}: {bug.web_link}")
% (bug.id, bug.web_link))

View File

@ -20,12 +20,13 @@
# Please see the /usr/share/common-licenses/GPL-2 file for the full text # Please see the /usr/share/common-licenses/GPL-2 file for the full text
# of the GNU General Public License license. # of the GNU General Public License license.
import logging
import os import os
import re import re
import sys
import smtplib import smtplib
import socket import socket
import subprocess import subprocess
import sys
import tempfile import tempfile
from debian.changelog import Changelog from debian.changelog import Changelog
@ -33,19 +34,18 @@ from distro_info import DebianDistroInfo, DistroDataOutdated
from ubuntutools.archive import DebianSourcePackage, UbuntuSourcePackage from ubuntutools.archive import DebianSourcePackage, UbuntuSourcePackage
from ubuntutools.lp.udtexceptions import PackageNotFoundException from ubuntutools.lp.udtexceptions import PackageNotFoundException
from ubuntutools.question import confirmation_prompt, YesNoQuestion from ubuntutools.question import YesNoQuestion, confirmation_prompt
import logging
Logger = logging.getLogger(__name__) Logger = logging.getLogger(__name__)
__all__ = [ __all__ = [
'get_debian_srcpkg', "get_debian_srcpkg",
'get_ubuntu_srcpkg', "get_ubuntu_srcpkg",
'need_sponsorship', "need_sponsorship",
'check_existing_reports', "check_existing_reports",
'get_ubuntu_delta_changelog', "get_ubuntu_delta_changelog",
'mail_bug', "mail_bug",
] ]
@ -67,73 +67,86 @@ def get_ubuntu_srcpkg(name, release):
def need_sponsorship(name, component, release): def need_sponsorship(name, component, release):
''' """
Ask the user if he has upload permissions for the package or the Ask the user if he has upload permissions for the package or the
component. component.
''' """
val = YesNoQuestion().ask("Do you have upload permissions for the '%s' component or " val = YesNoQuestion().ask(
"the package '%s' in Ubuntu %s?\nIf in doubt answer 'n'." % f"Do you have upload permissions for the '{component}' component or "
(component, name, release), 'no') f"the package '{name}' in Ubuntu {release}?\nIf in doubt answer 'n'.",
return val == 'no' "no",
)
return val == "no"
def check_existing_reports(srcpkg): def check_existing_reports(srcpkg):
''' """
Point the user to the URL to manually check for duplicate bug reports. Point the user to the URL to manually check for duplicate bug reports.
''' """
print('Please check on ' print(
'https://bugs.launchpad.net/ubuntu/+source/%s/+bugs\n' f"Please check on https://bugs.launchpad.net/ubuntu/+source/{srcpkg}/+bugs\n"
'for duplicate sync requests before continuing.' % srcpkg) f"for duplicate sync requests before continuing."
)
confirmation_prompt() confirmation_prompt()
def get_ubuntu_delta_changelog(srcpkg): def get_ubuntu_delta_changelog(srcpkg):
''' """
Download the Ubuntu changelog and extract the entries since the last sync Download the Ubuntu changelog and extract the entries since the last sync
from Debian. from Debian.
''' """
changelog = Changelog(srcpkg.getChangelog()) changelog = Changelog(srcpkg.getChangelog())
if changelog is None: if changelog is None:
return '' return ""
delta = [] delta = []
debian_info = DebianDistroInfo() debian_info = DebianDistroInfo()
for block in changelog: for block in changelog:
distribution = block.distributions.split()[0].split('-')[0] distribution = block.distributions.split()[0].split("-")[0]
if debian_info.valid(distribution): if debian_info.valid(distribution):
break break
delta += [str(change) for change in block.changes() delta += [str(change) for change in block.changes() if change.strip()]
if change.strip()]
return '\n'.join(delta) return "\n".join(delta)
def mail_bug(srcpkg, subscribe, status, bugtitle, bugtext, bug_mail_domain, def mail_bug(
keyid, myemailaddr, mailserver_host, mailserver_port, srcpkg,
mailserver_user, mailserver_pass): subscribe,
''' status,
bugtitle,
bugtext,
bug_mail_domain,
keyid,
myemailaddr,
mailserver_host,
mailserver_port,
mailserver_user,
mailserver_pass,
):
"""
Submit the sync request per email. Submit the sync request per email.
''' """
to = 'new@' + bug_mail_domain to = f"new@{bug_mail_domain}"
# generate mailbody # generate mailbody
if srcpkg: if srcpkg:
mailbody = ' affects ubuntu/%s\n' % srcpkg mailbody = f" affects ubuntu/{srcpkg}\n"
else: else:
mailbody = ' affects ubuntu\n' mailbody = " affects ubuntu\n"
mailbody += '''\ mailbody += f"""\
status %s status {status}
importance wishlist importance wishlist
subscribe %s subscribe {subscribe}
done done
%s''' % (status, subscribe, bugtext) {bugtext}"""
# prepare sign command # prepare sign command
gpg_command = None gpg_command = None
for cmd in ('gnome-gpg', 'gpg2', 'gpg'): for cmd in ("gnome-gpg", "gpg2", "gpg"):
if os.access('/usr/bin/%s' % cmd, os.X_OK): if os.access(f"/usr/bin/{cmd}", os.X_OK):
gpg_command = [cmd] gpg_command = [cmd]
break break
@ -141,107 +154,130 @@ def mail_bug(srcpkg, subscribe, status, bugtitle, bugtext, bug_mail_domain,
Logger.error("Cannot locate gpg, please install the 'gnupg' package!") Logger.error("Cannot locate gpg, please install the 'gnupg' package!")
sys.exit(1) sys.exit(1)
gpg_command.append('--clearsign') gpg_command.append("--clearsign")
if keyid: if keyid:
gpg_command.extend(('-u', keyid)) gpg_command.extend(("-u", keyid))
# sign the mail body # sign the mail body
gpg = subprocess.Popen( gpg = subprocess.Popen(
gpg_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, gpg_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, encoding="utf-8"
encoding='utf-8') )
signed_report = gpg.communicate(mailbody)[0] signed_report = gpg.communicate(mailbody)[0]
if gpg.returncode != 0: if gpg.returncode != 0:
Logger.error("%s failed.", gpg_command[0]) Logger.error("%s failed.", gpg_command[0])
sys.exit(1) sys.exit(1)
# generate email # generate email
mail = '''\ mail = f"""\
From: %s From: {myemailaddr}
To: %s To: {to}
Subject: %s Subject: {bugtitle}
Content-Type: text/plain; charset=UTF-8 Content-Type: text/plain; charset=UTF-8
%s''' % (myemailaddr, to, bugtitle, signed_report) {signed_report}"""
print('The final report is:\n%s' % mail) print(f"The final report is:\n{mail}")
confirmation_prompt() confirmation_prompt()
# save mail in temporary file # save mail in temporary file
backup = tempfile.NamedTemporaryFile( backup = tempfile.NamedTemporaryFile(
mode='w', mode="w",
delete=False, delete=False,
prefix='requestsync-' + re.sub(r'[^a-zA-Z0-9_-]', '', bugtitle.replace(' ', '_')) prefix=f"requestsync-{re.sub('[^a-zA-Z0-9_-]', '', bugtitle.replace(' ', '_'))}",
) )
with backup: with backup:
backup.write(mail) backup.write(mail)
Logger.info('The e-mail has been saved in %s and will be deleted ' Logger.info(
'after succesful transmission', backup.name) "The e-mail has been saved in %s and will be deleted after succesful transmission",
backup.name,
)
# connect to the server # connect to the server
while True: while True:
try: try:
Logger.info('Connecting to %s:%s ...', mailserver_host, Logger.info("Connecting to %s:%s ...", mailserver_host, mailserver_port)
mailserver_port) smtp = smtplib.SMTP(mailserver_host, mailserver_port)
s = smtplib.SMTP(mailserver_host, mailserver_port)
break break
except smtplib.SMTPConnectError as s: except smtplib.SMTPConnectError as error:
try: try:
# py2 path # py2 path
# pylint: disable=unsubscriptable-object # pylint: disable=unsubscriptable-object
Logger.error('Could not connect to %s:%s: %s (%i)', Logger.error(
mailserver_host, mailserver_port, s[1], s[0]) "Could not connect to %s:%s: %s (%i)",
mailserver_host,
mailserver_port,
error[1],
error[0],
)
except TypeError: except TypeError:
# pylint: disable=no-member # pylint: disable=no-member
Logger.error('Could not connect to %s:%s: %s (%i)', Logger.error(
mailserver_host, mailserver_port, s.strerror, s.errno) "Could not connect to %s:%s: %s (%i)",
if s.smtp_code == 421: mailserver_host,
confirmation_prompt(message='This is a temporary error, press [Enter] ' mailserver_port,
'to retry. Press [Ctrl-C] to abort now.') error.strerror,
except socket.error as s: error.errno,
)
if error.smtp_code == 421:
confirmation_prompt(
message="This is a temporary error, press [Enter] "
"to retry. Press [Ctrl-C] to abort now."
)
except socket.error as error:
try: try:
# py2 path # py2 path
# pylint: disable=unsubscriptable-object # pylint: disable=unsubscriptable-object
Logger.error('Could not connect to %s:%s: %s (%i)', Logger.error(
mailserver_host, mailserver_port, s[1], s[0]) "Could not connect to %s:%s: %s (%i)",
mailserver_host,
mailserver_port,
error[1],
error[0],
)
except TypeError: except TypeError:
# pylint: disable=no-member # pylint: disable=no-member
Logger.error('Could not connect to %s:%s: %s (%i)', Logger.error(
mailserver_host, mailserver_port, s.strerror, s.errno) "Could not connect to %s:%s: %s (%i)",
mailserver_host,
mailserver_port,
error.strerror,
error.errno,
)
return return
if mailserver_user and mailserver_pass: if mailserver_user and mailserver_pass:
try: try:
s.login(mailserver_user, mailserver_pass) smtp.login(mailserver_user, mailserver_pass)
except smtplib.SMTPAuthenticationError: except smtplib.SMTPAuthenticationError:
Logger.error('Error authenticating to the server: ' Logger.error("Error authenticating to the server: invalid username and password.")
'invalid username and password.') smtp.quit()
s.quit()
return return
except smtplib.SMTPException: except smtplib.SMTPException:
Logger.error('Unknown SMTP error.') Logger.error("Unknown SMTP error.")
s.quit() smtp.quit()
return return
while True: while True:
try: try:
s.sendmail(myemailaddr, to, mail.encode('utf-8')) smtp.sendmail(myemailaddr, to, mail.encode("utf-8"))
s.quit() smtp.quit()
os.remove(backup.name) os.remove(backup.name)
Logger.info('Sync request mailed.') Logger.info("Sync request mailed.")
break break
except smtplib.SMTPRecipientsRefused as smtperror: except smtplib.SMTPRecipientsRefused as smtperror:
smtp_code, smtp_message = smtperror.recipients[to] smtp_code, smtp_message = smtperror.recipients[to]
Logger.error('Error while sending: %i, %s', smtp_code, smtp_message) Logger.error("Error while sending: %i, %s", smtp_code, smtp_message)
if smtp_code == 450: if smtp_code == 450:
confirmation_prompt(message='This is a temporary error, press [Enter] ' confirmation_prompt(
'to retry. Press [Ctrl-C] to abort now.') message="This is a temporary error, press [Enter] "
"to retry. Press [Ctrl-C] to abort now."
)
else: else:
return return
except smtplib.SMTPResponseException as e: except smtplib.SMTPResponseException as error:
Logger.error('Error while sending: %i, %s', Logger.error("Error while sending: %i, %s", error.smtp_code, error.smtp_error)
e.smtp_code, e.smtp_error)
return return
except smtplib.SMTPServerDisconnected: except smtplib.SMTPServerDisconnected:
Logger.error('Server disconnected while sending the mail.') Logger.error("Server disconnected while sending the mail.")
return return

View File

@ -0,0 +1,95 @@
# Copyright (C) 2024 Canonical Ltd.
# Author: Chris Peterson <chris.peterson@canonical.com>
# Author: Andy P. Whitcroft
# Author: Christian Ehrhardt
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
import json
import sys
import urllib
import urllib.request
URL_RUNNING = "http://autopkgtest.ubuntu.com/static/running.json"
URL_QUEUED = "http://autopkgtest.ubuntu.com/queues.json"
def _get_jobs(url: str) -> dict:
request = urllib.request.Request(url, headers={"Cache-Control": "max-age-0"})
with urllib.request.urlopen(request) as response:
data = response.read()
jobs = json.loads(data.decode("utf-8"))
return jobs
def get_running():
jobs = _get_jobs(URL_RUNNING)
running = []
for pkg in jobs:
for handle in jobs[pkg]:
for series in jobs[pkg][handle]:
for arch in jobs[pkg][handle][series]:
jobinfo = jobs[pkg][handle][series][arch]
triggers = ",".join(jobinfo[0].get("triggers", "-"))
ppas = ",".join(jobinfo[0].get("ppas", "-"))
time = jobinfo[1]
env = jobinfo[0].get("env", "-")
time = str(datetime.timedelta(seconds=jobinfo[1]))
try:
line = (
f"R {time:6} {pkg:30} {'-':10} {series:8} {arch:8}"
f" {ppas:31} {triggers} {env}\n"
)
running.append((jobinfo[1], line))
except BrokenPipeError:
sys.exit(1)
output = ""
for time, row in sorted(running, reverse=True):
output += f"{row}"
return output
def get_queued():
queues = _get_jobs(URL_QUEUED)
output = ""
for origin in queues:
for series in queues[origin]:
for arch in queues[origin][series]:
n = 0
for key in queues[origin][series][arch]:
if key == "private job":
pkg = triggers = ppas = "private job"
else:
(pkg, json_data) = key.split(maxsplit=1)
try:
jobinfo = json.loads(json_data)
triggers = ",".join(jobinfo.get("triggers", "-"))
ppas = ",".join(jobinfo.get("ppas", "-"))
except json.decoder.JSONDecodeError:
pkg = triggers = ppas = "failed to parse"
continue
n = n + 1
try:
output += (
f"Q{n:04d} {'-:--':>6} {pkg:30} {origin:10} {series:8} {arch:8}"
f" {ppas:31} {triggers}\n"
)
except BrokenPipeError:
sys.exit(1)
return output

View File

@ -15,6 +15,7 @@
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
import logging
import os import os
import re import re
from urllib.parse import unquote from urllib.parse import unquote
@ -25,7 +26,6 @@ import httplib2
from ubuntutools.version import Version from ubuntutools.version import Version
import logging
Logger = logging.getLogger(__name__) Logger = logging.getLogger(__name__)
@ -37,7 +37,7 @@ def is_sync(bug):
return "sync" in bug.title.lower().split(" ") or "sync" in bug.tags return "sync" in bug.title.lower().split(" ") or "sync" in bug.tags
class BugTask(object): class BugTask:
def __init__(self, bug_task, launchpad): def __init__(self, bug_task, launchpad):
self.bug_task = bug_task self.bug_task = bug_task
self.launchpad = launchpad self.launchpad = launchpad
@ -58,7 +58,7 @@ class BugTask(object):
self.series = components[2].lower() self.series = components[2].lower()
if self.package is None: if self.package is None:
title_re = r'^Sync ([a-z0-9+.-]+) [a-z0-9.+:~-]+ \([a-z]+\) from.*' title_re = r"^Sync ([a-z0-9+.-]+) [a-z0-9.+:~-]+ \([a-z]+\) from.*"
match = re.match(title_re, self.get_bug_title(), re.U | re.I) match = re.match(title_re, self.get_bug_title(), re.U | re.I)
if match is not None: if match is not None:
self.package = match.group(1) self.package = match.group(1)
@ -68,34 +68,42 @@ class BugTask(object):
dsc_file = "" dsc_file = ""
for url in source_files: for url in source_files:
filename = unquote(os.path.basename(url)) filename = unquote(os.path.basename(url))
Logger.debug("Downloading %s..." % (filename)) Logger.debug("Downloading %s...", filename)
# HttpLib2 isn't suitable for large files (it reads into memory), # HttpLib2 isn't suitable for large files (it reads into memory),
# but we want its https certificate validation on the .dsc # but we want its https certificate validation on the .dsc
if url.endswith(".dsc"): if url.endswith(".dsc"):
response, data = httplib2.Http().request(url) response, data = httplib2.Http().request(url)
assert response.status == 200 assert response.status == 200
with open(filename, 'wb') as f: with open(filename, "wb") as f:
f.write(data) f.write(data)
dsc_file = os.path.join(os.getcwd(), filename) dsc_file = os.path.join(os.getcwd(), filename)
else: else:
urlretrieve(url, filename) urlretrieve(url, filename)
assert os.path.isfile(dsc_file), "%s does not exist." % (dsc_file) assert os.path.isfile(dsc_file), f"{dsc_file} does not exist."
return dsc_file return dsc_file
def get_branch_link(self): def get_branch_link(self):
return "lp:" + self.project + "/" + self.get_series() + "/" + \ return "lp:" + self.project + "/" + self.get_series() + "/" + self.package
self.package
def get_bug_title(self): def get_bug_title(self):
"""Returns the title of the related bug.""" """Returns the title of the related bug."""
return self.bug_task.bug.title return self.bug_task.bug.title
def get_long_info(self): def get_long_info(self):
return "Bug task: " + str(self.bug_task) + "\n" + \ return (
"Package: " + str(self.package) + "\n" + \ "Bug task: "
"Project: " + str(self.project) + "\n" + \ + str(self.bug_task)
"Series: " + str(self.series) + "\n"
+ "Package: "
+ str(self.package)
+ "\n"
+ "Project: "
+ str(self.project)
+ "\n"
+ "Series: "
+ str(self.series)
)
def get_lp_task(self): def get_lp_task(self):
"""Returns the Launchpad bug task object.""" """Returns the Launchpad bug task object."""
@ -118,8 +126,7 @@ class BugTask(object):
if self.series is None or latest_release: if self.series is None or latest_release:
dist = self.launchpad.distributions[self.project] dist = self.launchpad.distributions[self.project]
return dist.current_series.name return dist.current_series.name
else: return self.series
return self.series
def get_short_info(self): def get_short_info(self):
return self.bug_task.bug_target_name + ": " + self.bug_task.status return self.bug_task.bug_target_name + ": " + self.bug_task.status
@ -137,14 +144,16 @@ class BugTask(object):
dist = self.launchpad.distributions[project] dist = self.launchpad.distributions[project]
archive = dist.getArchive(name="primary") archive = dist.getArchive(name="primary")
distro_series = dist.getSeries(name_or_version=series) distro_series = dist.getSeries(name_or_version=series)
published = archive.getPublishedSources(source_name=self.package, published = archive.getPublishedSources(
distro_series=distro_series, source_name=self.package,
status="Published", distro_series=distro_series,
exact_match=True) status="Published",
exact_match=True,
)
latest_source = None latest_source = None
for source in published: for source in published:
if source.pocket in ('Release', 'Security', 'Updates', 'Proposed'): if source.pocket in ("Release", "Security", "Updates", "Proposed"):
latest_source = source latest_source = source
break break
return latest_source return latest_source
@ -156,7 +165,7 @@ class BugTask(object):
def get_latest_released_version(self): def get_latest_released_version(self):
source = self.get_source(True) source = self.get_source(True)
if source is None: # Not currently published in Ubuntu if source is None: # Not currently published in Ubuntu
version = '~' version = "~"
else: else:
version = source.source_package_version version = source.source_package_version
return Version(version) return Version(version)

View File

@ -15,27 +15,27 @@
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
import logging
import os import os
import re import re
import subprocess import subprocess
from ubuntutools.sponsor_patch.question import ask_for_manual_fixing
from functools import reduce from functools import reduce
import logging from ubuntutools.sponsor_patch.question import ask_for_manual_fixing
Logger = logging.getLogger(__name__) Logger = logging.getLogger(__name__)
class Patch(object): class Patch:
"""This object represents a patch that can be downloaded from Launchpad.""" """This object represents a patch that can be downloaded from Launchpad."""
def __init__(self, patch): def __init__(self, patch):
self._patch = patch self._patch = patch
self._patch_file = re.sub(" |/", "_", patch.title) self._patch_file = re.sub(" |/", "_", patch.title)
if not reduce(lambda r, x: r or self._patch.title.endswith(x), if not reduce(
(".debdiff", ".diff", ".patch"), False): lambda r, x: r or self._patch.title.endswith(x), (".debdiff", ".diff", ".patch"), False
Logger.debug("Patch %s does not have a proper file extension." % ):
(self._patch.title)) Logger.debug("Patch %s does not have a proper file extension.", self._patch.title)
self._patch_file += ".patch" self._patch_file += ".patch"
self._full_path = os.path.realpath(self._patch_file) self._full_path = os.path.realpath(self._patch_file)
self._changed_files = None self._changed_files = None
@ -45,21 +45,36 @@ class Patch(object):
assert self._changed_files is not None, "You forgot to download the patch." assert self._changed_files is not None, "You forgot to download the patch."
edit = False edit = False
if self.is_debdiff(): if self.is_debdiff():
cmd = ["patch", "--merge", "--force", "-p", cmd = [
str(self.get_strip_level()), "-i", self._full_path] "patch",
Logger.debug(' '.join(cmd)) "--merge",
"--force",
"-p",
str(self.get_strip_level()),
"-i",
self._full_path,
]
Logger.debug(" ".join(cmd))
if subprocess.call(cmd) != 0: if subprocess.call(cmd) != 0:
Logger.error("Failed to apply debdiff %s to %s %s.", Logger.error(
self._patch_file, task.package, task.get_version()) "Failed to apply debdiff %s to %s %s.",
self._patch_file,
task.package,
task.get_version(),
)
if not edit: if not edit:
ask_for_manual_fixing() ask_for_manual_fixing()
edit = True edit = True
else: else:
cmd = ["add-patch", self._full_path] cmd = ["add-patch", self._full_path]
Logger.debug(' '.join(cmd)) Logger.debug(" ".join(cmd))
if subprocess.call(cmd) != 0: if subprocess.call(cmd) != 0:
Logger.error("Failed to apply diff %s to %s %s.", Logger.error(
self._patch_file, task.package, task.get_version()) "Failed to apply diff %s to %s %s.",
self._patch_file,
task.package,
task.get_version(),
)
if not edit: if not edit:
ask_for_manual_fixing() ask_for_manual_fixing()
edit = True edit = True
@ -67,13 +82,13 @@ class Patch(object):
def download(self): def download(self):
"""Downloads the patch from Launchpad.""" """Downloads the patch from Launchpad."""
Logger.debug("Downloading %s." % (self._patch_file)) Logger.debug("Downloading %s.", self._patch_file)
patch_f = open(self._patch_file, "wb") patch_f = open(self._patch_file, "wb")
patch_f.write(self._patch.data.open().read()) patch_f.write(self._patch.data.open().read())
patch_f.close() patch_f.close()
cmd = ["diffstat", "-l", "-p0", self._full_path] cmd = ["diffstat", "-l", "-p0", self._full_path]
changed_files = subprocess.check_output(cmd, encoding='utf-8') changed_files = subprocess.check_output(cmd, encoding="utf-8")
self._changed_files = [f for f in changed_files.split("\n") if f != ""] self._changed_files = [f for f in changed_files.split("\n") if f != ""]
def get_strip_level(self): def get_strip_level(self):
@ -81,13 +96,11 @@ class Patch(object):
assert self._changed_files is not None, "You forgot to download the patch." assert self._changed_files is not None, "You forgot to download the patch."
strip_level = None strip_level = None
if self.is_debdiff(): if self.is_debdiff():
changelog = [f for f in self._changed_files changelog = [f for f in self._changed_files if f.endswith("debian/changelog")][0]
if f.endswith("debian/changelog")][0]
strip_level = len(changelog.split(os.sep)) - 2 strip_level = len(changelog.split(os.sep)) - 2
return strip_level return strip_level
def is_debdiff(self): def is_debdiff(self):
"""Checks if the patch is a debdiff (= modifies debian/changelog).""" """Checks if the patch is a debdiff (= modifies debian/changelog)."""
assert self._changed_files is not None, "You forgot to download the patch." assert self._changed_files is not None, "You forgot to download the patch."
return len([f for f in self._changed_files return len([f for f in self._changed_files if f.endswith("debian/changelog")]) > 0
if f.endswith("debian/changelog")]) > 0

View File

@ -37,8 +37,7 @@ def ask_for_ignoring_or_fixing():
def ask_for_manual_fixing(): def ask_for_manual_fixing():
"""Ask the user to resolve an issue manually.""" """Ask the user to resolve an issue manually."""
answer = YesNoQuestion().ask("Do you want to resolve this issue manually", answer = YesNoQuestion().ask("Do you want to resolve this issue manually", "yes")
"yes")
if answer == "no": if answer == "no":
user_abort() user_abort()

View File

@ -15,6 +15,7 @@
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
import logging
import os import os
import re import re
import subprocess import subprocess
@ -24,21 +25,22 @@ import debian.changelog
import debian.deb822 import debian.deb822
from ubuntutools.question import Question, YesNoQuestion from ubuntutools.question import Question, YesNoQuestion
from ubuntutools.sponsor_patch.question import (
ask_for_ignoring_or_fixing,
ask_for_manual_fixing,
user_abort,
)
from ubuntutools.sponsor_patch.question import (ask_for_ignoring_or_fixing,
ask_for_manual_fixing,
user_abort)
import logging
Logger = logging.getLogger(__name__) Logger = logging.getLogger(__name__)
def _get_series(launchpad): def _get_series(launchpad):
"""Returns a tuple with the development and list of supported series.""" """Returns a tuple with the development and list of supported series."""
ubuntu = launchpad.distributions['ubuntu'] ubuntu = launchpad.distributions["ubuntu"]
devel_series = ubuntu.current_series.name devel_series = ubuntu.current_series.name
supported_series = [series.name for series in ubuntu.series supported_series = [
if series.active and series.name != devel_series] series.name for series in ubuntu.series if series.active and series.name != devel_series
]
return (devel_series, supported_series) return (devel_series, supported_series)
@ -49,14 +51,14 @@ def strip_epoch(version):
return "1.1.3-1". return "1.1.3-1".
""" """
parts = version.full_version.split(':') parts = version.full_version.split(":")
if len(parts) > 1: if len(parts) > 1:
del parts[0] del parts[0]
version_without_epoch = ':'.join(parts) version_without_epoch = ":".join(parts)
return version_without_epoch return version_without_epoch
class SourcePackage(object): class SourcePackage:
"""This class represents a source package.""" """This class represents a source package."""
def __init__(self, package, builder, workdir, branch): def __init__(self, package, builder, workdir, branch):
@ -74,11 +76,10 @@ class SourcePackage(object):
if upload == "ubuntu": if upload == "ubuntu":
self._print_logs() self._print_logs()
question = Question(["yes", "edit", "no"]) question = Question(["yes", "edit", "no"])
answer = question.ask("Do you want to acknowledge the sync request", answer = question.ask("Do you want to acknowledge the sync request", "no")
"no")
if answer == "edit": if answer == "edit":
return False return False
elif answer == "no": if answer == "no":
user_abort() user_abort()
bug = task.bug bug = task.bug
@ -90,33 +91,32 @@ class SourcePackage(object):
msg = "Sync request ACK'd." msg = "Sync request ACK'd."
if self._build_log: if self._build_log:
msg = ("%s %s builds on %s. " + msg) % \ msg = (
(self._package, self._version, f"{self._package} {self._version} builds"
self._builder.get_architecture()) f" on {self._builder.get_architecture()}. {msg}"
)
bug.newMessage(content=msg, subject="sponsor-patch") bug.newMessage(content=msg, subject="sponsor-patch")
Logger.debug("Acknowledged sync request bug #%i.", bug.id) Logger.debug("Acknowledged sync request bug #%i.", bug.id)
bug.subscribe(person=launchpad.people['ubuntu-archive']) bug.subscribe(person=launchpad.people["ubuntu-archive"])
Logger.debug("Subscribed ubuntu-archive to bug #%i.", bug.id) Logger.debug("Subscribed ubuntu-archive to bug #%i.", bug.id)
bug.subscribe(person=launchpad.me) bug.subscribe(person=launchpad.me)
Logger.debug("Subscribed me to bug #%i.", bug.id) Logger.debug("Subscribed me to bug #%i.", bug.id)
sponsorsteam = launchpad.people['ubuntu-sponsors'] sponsorsteam = launchpad.people["ubuntu-sponsors"]
for sub in bug.subscriptions: for sub in bug.subscriptions:
if sub.person == sponsorsteam and sub.canBeUnsubscribedByUser(): if sub.person == sponsorsteam and sub.canBeUnsubscribedByUser():
bug.unsubscribe(person=launchpad.people['ubuntu-sponsors']) bug.unsubscribe(person=launchpad.people["ubuntu-sponsors"])
Logger.debug("Unsubscribed ubuntu-sponsors from bug #%i.", Logger.debug("Unsubscribed ubuntu-sponsors from bug #%i.", bug.id)
bug.id)
elif sub.person == sponsorsteam: elif sub.person == sponsorsteam:
Logger.debug("Couldn't unsubscribe ubuntu-sponsors from " Logger.debug("Couldn't unsubscribe ubuntu-sponsors from bug #%i.", bug.id)
"bug #%i.", bug.id)
Logger.info("Successfully acknowledged sync request bug #%i.", Logger.info("Successfully acknowledged sync request bug #%i.", bug.id)
bug.id)
else: else:
Logger.error("Sync requests can only be acknowledged when the " Logger.error(
"upload target is Ubuntu.") "Sync requests can only be acknowledged when the upload target is Ubuntu."
)
sys.exit(1) sys.exit(1)
return True return True
@ -135,34 +135,35 @@ class SourcePackage(object):
else: else:
target = upload target = upload
question = Question(["yes", "edit", "no"]) question = Question(["yes", "edit", "no"])
answer = question.ask("Do you want to upload the package to %s" % target, "no") answer = question.ask(f"Do you want to upload the package to {target}", "no")
if answer == "edit": if answer == "edit":
return False return False
elif answer == "no": if answer == "no":
user_abort() user_abort()
cmd = ["dput", "--force", upload, self._changes_file] cmd = ["dput", "--force", upload, self._changes_file]
Logger.debug(' '.join(cmd)) Logger.debug(" ".join(cmd))
if subprocess.call(cmd) != 0: if subprocess.call(cmd) != 0:
Logger.error("Upload of %s to %s failed." % Logger.error(
(os.path.basename(self._changes_file), upload)) "Upload of %s to %s failed.", os.path.basename(self._changes_file), upload
)
sys.exit(1) sys.exit(1)
# Push the branch if the package is uploaded to the Ubuntu archive. # Push the branch if the package is uploaded to the Ubuntu archive.
if upload == "ubuntu" and self._branch: if upload == "ubuntu" and self._branch:
cmd = ['debcommit'] cmd = ["debcommit"]
Logger.debug(' '.join(cmd)) Logger.debug(" ".join(cmd))
if subprocess.call(cmd) != 0: if subprocess.call(cmd) != 0:
Logger.error('Bzr commit failed.') Logger.error("Bzr commit failed.")
sys.exit(1) sys.exit(1)
cmd = ['bzr', 'mark-uploaded'] cmd = ["bzr", "mark-uploaded"]
Logger.debug(' '.join(cmd)) Logger.debug(" ".join(cmd))
if subprocess.call(cmd) != 0: if subprocess.call(cmd) != 0:
Logger.error('Bzr tagging failed.') Logger.error("Bzr tagging failed.")
sys.exit(1) sys.exit(1)
cmd = ['bzr', 'push', ':parent'] cmd = ["bzr", "push", ":parent"]
Logger.debug(' '.join(cmd)) Logger.debug(" ".join(cmd))
if subprocess.call(cmd) != 0: if subprocess.call(cmd) != 0:
Logger.error('Bzr push failed.') Logger.error("Bzr push failed.")
sys.exit(1) sys.exit(1)
return True return True
@ -175,8 +176,10 @@ class SourcePackage(object):
if dist is None: if dist is None:
dist = re.sub("-.*$", "", self._changelog.distributions) dist = re.sub("-.*$", "", self._changelog.distributions)
build_name = "{}_{}_{}.build".format(self._package, strip_epoch(self._version), build_name = (
self._builder.get_architecture()) f"{self._package}_{strip_epoch(self._version)}"
f"_{self._builder.get_architecture()}.build"
)
self._build_log = os.path.join(self._buildresult, build_name) self._build_log = os.path.join(self._buildresult, build_name)
successful_built = False successful_built = False
@ -191,20 +194,18 @@ class SourcePackage(object):
update = False update = False
# build package # build package
result = self._builder.build(self._dsc_file, dist, result = self._builder.build(self._dsc_file, dist, self._buildresult)
self._buildresult)
if result != 0: if result != 0:
question = Question(["yes", "update", "retry", "no"]) question = Question(["yes", "update", "retry", "no"])
answer = question.ask("Do you want to resolve this issue manually", "yes") answer = question.ask("Do you want to resolve this issue manually", "yes")
if answer == "yes": if answer == "yes":
break break
elif answer == "update": if answer == "update":
update = True update = True
continue continue
elif answer == "retry": if answer == "retry":
continue continue
else: user_abort()
user_abort()
successful_built = True successful_built = True
if not successful_built: if not successful_built:
# We want to do a manual fix if the build failed. # We want to do a manual fix if the build failed.
@ -224,13 +225,14 @@ class SourcePackage(object):
""" """
if self._branch: if self._branch:
cmd = ['bzr', 'builddeb', '--builder=debuild', '-S', cmd = ["bzr", "builddeb", "--builder=debuild", "-S", "--", "--no-lintian", "-nc"]
'--', '--no-lintian', '-nc']
else: else:
cmd = ['debuild', '--no-lintian', '-nc', '-S'] cmd = ["debuild", "--no-lintian", "-nc", "-S"]
cmd.append("-v" + previous_version.full_version) cmd.append("-v" + previous_version.full_version)
if previous_version.upstream_version == \ if (
self._changelog.upstream_version and upload == "ubuntu": previous_version.upstream_version == self._changelog.upstream_version
and upload == "ubuntu"
):
# FIXME: Add proper check that catches cases like changed # FIXME: Add proper check that catches cases like changed
# compression (.tar.gz -> tar.bz2) and multiple orig source tarballs # compression (.tar.gz -> tar.bz2) and multiple orig source tarballs
cmd.append("-sd") cmd.append("-sd")
@ -239,9 +241,9 @@ class SourcePackage(object):
if keyid is not None: if keyid is not None:
cmd += ["-k" + keyid] cmd += ["-k" + keyid]
env = os.environ env = os.environ
if upload == 'ubuntu': if upload == "ubuntu":
env['DEB_VENDOR'] = 'Ubuntu' env["DEB_VENDOR"] = "Ubuntu"
Logger.debug(' '.join(cmd)) Logger.debug(" ".join(cmd))
if subprocess.call(cmd, env=env) != 0: if subprocess.call(cmd, env=env) != 0:
Logger.error("Failed to build source tarball.") Logger.error("Failed to build source tarball.")
# TODO: Add a "retry" option # TODO: Add a "retry" option
@ -252,8 +254,9 @@ class SourcePackage(object):
@property @property
def _changes_file(self): def _changes_file(self):
"""Returns the file name of the .changes file.""" """Returns the file name of the .changes file."""
return os.path.join(self._workdir, "{}_{}_source.changes" return os.path.join(
.format(self._package, strip_epoch(self._version))) self._workdir, f"{self._package}_{strip_epoch(self._version)}_source.changes"
)
def check_target(self, upload, launchpad): def check_target(self, upload, launchpad):
"""Make sure that the target is correct. """Make sure that the target is correct.
@ -265,18 +268,24 @@ class SourcePackage(object):
(devel_series, supported_series) = _get_series(launchpad) (devel_series, supported_series) = _get_series(launchpad)
if upload == "ubuntu": if upload == "ubuntu":
allowed = supported_series + \ allowed = (
[s + "-proposed" for s in supported_series] + \ supported_series + [s + "-proposed" for s in supported_series] + [devel_series]
[devel_series] )
if self._changelog.distributions not in allowed: if self._changelog.distributions not in allowed:
Logger.error("%s is not an allowed series. It needs to be one of %s." % Logger.error(
(self._changelog.distributions, ", ".join(allowed))) "%s is not an allowed series. It needs to be one of %s.",
self._changelog.distributions,
", ".join(allowed),
)
return ask_for_ignoring_or_fixing() return ask_for_ignoring_or_fixing()
elif upload and upload.startswith("ppa/"): elif upload and upload.startswith("ppa/"):
allowed = supported_series + [devel_series] allowed = supported_series + [devel_series]
if self._changelog.distributions not in allowed: if self._changelog.distributions not in allowed:
Logger.error("%s is not an allowed series. It needs to be one of %s." % Logger.error(
(self._changelog.distributions, ", ".join(allowed))) "%s is not an allowed series. It needs to be one of %s.",
self._changelog.distributions,
", ".join(allowed),
)
return ask_for_ignoring_or_fixing() return ask_for_ignoring_or_fixing()
return True return True
@ -288,18 +297,21 @@ class SourcePackage(object):
""" """
if self._version <= previous_version: if self._version <= previous_version:
Logger.error("The version %s is not greater than the already " Logger.error(
"available %s.", self._version, previous_version) "The version %s is not greater than the already available %s.",
self._version,
previous_version,
)
return ask_for_ignoring_or_fixing() return ask_for_ignoring_or_fixing()
return True return True
def check_sync_request_version(self, bug_number, task): def check_sync_request_version(self, bug_number, task):
"""Check if the downloaded version of the package is mentioned in the """Check if the downloaded version of the package is mentioned in the
bug title.""" bug title."""
if not task.title_contains(self._version): if not task.title_contains(self._version):
print("Bug #%i title: %s" % (bug_number, task.get_bug_title())) print(f"Bug #{bug_number} title: {task.get_bug_title()}")
msg = "Is %s %s the version that should be synced" % (self._package, self._version) msg = f"Is {self._package} {self._version} the version that should be synced"
answer = YesNoQuestion().ask(msg, "no") answer = YesNoQuestion().ask(msg, "no")
if answer == "no": if answer == "no":
user_abort() user_abort()
@ -307,32 +319,27 @@ class SourcePackage(object):
@property @property
def _debdiff_filename(self): def _debdiff_filename(self):
"""Returns the file name of the .debdiff file.""" """Returns the file name of the .debdiff file."""
debdiff_name = "{}_{}.debdiff".format(self._package, strip_epoch(self._version)) debdiff_name = f"{self._package}_{strip_epoch(self._version)}.debdiff"
return os.path.join(self._workdir, debdiff_name) return os.path.join(self._workdir, debdiff_name)
@property @property
def _dsc_file(self): def _dsc_file(self):
"""Returns the file name of the .dsc file.""" """Returns the file name of the .dsc file."""
return os.path.join(self._workdir, "{}_{}.dsc".format(self._package, return os.path.join(self._workdir, f"{self._package}_{strip_epoch(self._version)}.dsc")
strip_epoch(self._version)))
def generate_debdiff(self, dsc_file): def generate_debdiff(self, dsc_file):
"""Generates a debdiff between the given .dsc file and this source """Generates a debdiff between the given .dsc file and this source
package.""" package."""
assert os.path.isfile(dsc_file), "%s does not exist." % (dsc_file) assert os.path.isfile(dsc_file), f"{dsc_file} does not exist."
assert os.path.isfile(self._dsc_file), "%s does not exist." % \ assert os.path.isfile(self._dsc_file), f"{self._dsc_file} does not exist."
(self._dsc_file)
cmd = ["debdiff", dsc_file, self._dsc_file] cmd = ["debdiff", dsc_file, self._dsc_file]
if not Logger.isEnabledFor(logging.DEBUG): if not Logger.isEnabledFor(logging.DEBUG):
cmd.insert(1, "-q") cmd.insert(1, "-q")
Logger.debug(' '.join(cmd) + " > " + self._debdiff_filename) Logger.debug("%s > %s", " ".join(cmd), self._debdiff_filename)
debdiff = subprocess.check_output(cmd, encoding='utf-8') with open(self._debdiff_filename, "w", encoding="utf-8") as debdiff_file:
debdiff = subprocess.run(cmd, check=False, stdout=debdiff_file)
# write debdiff file assert debdiff.returncode in (0, 1)
debdiff_file = open(self._debdiff_filename, "w")
debdiff_file.writelines(debdiff)
debdiff_file.close()
def is_fixed(self, lp_bug): def is_fixed(self, lp_bug):
"""Make sure that the given Launchpad bug is closed. """Make sure that the given Launchpad bug is closed.
@ -341,8 +348,8 @@ class SourcePackage(object):
change something. change something.
""" """
assert os.path.isfile(self._changes_file), "%s does not exist." % (self._changes_file) assert os.path.isfile(self._changes_file), f"{self._changes_file} does not exist."
changes = debian.deb822.Changes(open(self._changes_file)) changes = debian.deb822.Changes(open(self._changes_file, encoding="utf-8"))
fixed_bugs = [] fixed_bugs = []
if "Launchpad-Bugs-Fixed" in changes: if "Launchpad-Bugs-Fixed" in changes:
fixed_bugs = changes["Launchpad-Bugs-Fixed"].split(" ") fixed_bugs = changes["Launchpad-Bugs-Fixed"].split(" ")
@ -354,7 +361,7 @@ class SourcePackage(object):
lp_bug = lp_bug.duplicate_of lp_bug = lp_bug.duplicate_of
if lp_bug.id not in fixed_bugs: if lp_bug.id not in fixed_bugs:
Logger.error("Launchpad bug #%i is not closed by new version." % (lp_bug.id)) Logger.error("Launchpad bug #%i is not closed by new version.", lp_bug.id)
return ask_for_ignoring_or_fixing() return ask_for_ignoring_or_fixing()
return True return True
@ -362,7 +369,7 @@ class SourcePackage(object):
"""Print things that should be checked before uploading a package.""" """Print things that should be checked before uploading a package."""
lintian_filename = self._run_lintian() lintian_filename = self._run_lintian()
print("\nPlease check %s %s carefully:" % (self._package, self._version)) print(f"\nPlease check {self._package} {self._version} carefully:")
if os.path.isfile(self._debdiff_filename): if os.path.isfile(self._debdiff_filename):
print("file://" + self._debdiff_filename) print("file://" + self._debdiff_filename)
print("file://" + lintian_filename) print("file://" + lintian_filename)
@ -379,8 +386,9 @@ class SourcePackage(object):
# Check the changelog # Check the changelog
self._changelog = debian.changelog.Changelog() self._changelog = debian.changelog.Changelog()
try: try:
self._changelog.parse_changelog(open("debian/changelog"), self._changelog.parse_changelog(
max_blocks=1, strict=True) open("debian/changelog", encoding="utf-8"), max_blocks=1, strict=True
)
except debian.changelog.ChangelogParseError as error: except debian.changelog.ChangelogParseError as error:
Logger.error("The changelog entry doesn't validate: %s", str(error)) Logger.error("The changelog entry doesn't validate: %s", str(error))
ask_for_manual_fixing() ask_for_manual_fixing()
@ -390,8 +398,10 @@ class SourcePackage(object):
try: try:
self._version = self._changelog.get_version() self._version = self._changelog.get_version()
except IndexError: except IndexError:
Logger.error("Debian package version could not be determined. " Logger.error(
"debian/changelog is probably malformed.") "Debian package version could not be determined. "
"debian/changelog is probably malformed."
)
ask_for_manual_fixing() ask_for_manual_fixing()
return False return False
@ -405,25 +415,29 @@ class SourcePackage(object):
# Determine whether to use the source or binary build for lintian # Determine whether to use the source or binary build for lintian
if self._build_log: if self._build_log:
build_changes = self._package + "_" + strip_epoch(self._version) + \ build_changes = (
"_" + self._builder.get_architecture() + ".changes" self._package
+ "_"
+ strip_epoch(self._version)
+ "_"
+ self._builder.get_architecture()
+ ".changes"
)
changes_for_lintian = os.path.join(self._buildresult, build_changes) changes_for_lintian = os.path.join(self._buildresult, build_changes)
else: else:
changes_for_lintian = self._changes_file changes_for_lintian = self._changes_file
# Check lintian # Check lintian
assert os.path.isfile(changes_for_lintian), "%s does not exist." % \ assert os.path.isfile(changes_for_lintian), f"{changes_for_lintian} does not exist."
(changes_for_lintian) cmd = ["lintian", "-IE", "--pedantic", "-q", "--profile", "ubuntu", changes_for_lintian]
cmd = ["lintian", "-IE", "--pedantic", "-q", "--profile", "ubuntu", lintian_filename = os.path.join(
changes_for_lintian] self._workdir, self._package + "_" + strip_epoch(self._version) + ".lintian"
lintian_filename = os.path.join(self._workdir, )
self._package + "_" + Logger.debug("%s > %s", " ".join(cmd), lintian_filename)
strip_epoch(self._version) + ".lintian") report = subprocess.check_output(cmd, encoding="utf-8")
Logger.debug(' '.join(cmd) + " > " + lintian_filename)
report = subprocess.check_output(cmd, encoding='utf-8')
# write lintian report file # write lintian report file
lintian_file = open(lintian_filename, "w") lintian_file = open(lintian_filename, "w", encoding="utf-8")
lintian_file.writelines(report) lintian_file.writelines(report)
lintian_file.close() lintian_file.close()
@ -433,17 +447,25 @@ class SourcePackage(object):
"""Does a sync of the source package.""" """Does a sync of the source package."""
if upload == "ubuntu": if upload == "ubuntu":
cmd = ["syncpackage", self._package, "-b", str(bug_number), "-f", cmd = [
"-s", requester, "-V", str(self._version), "syncpackage",
"-d", series] self._package,
Logger.debug(' '.join(cmd)) "-b",
str(bug_number),
"-f",
"-s",
requester,
"-V",
str(self._version),
"-d",
series,
]
Logger.debug(" ".join(cmd))
if subprocess.call(cmd) != 0: if subprocess.call(cmd) != 0:
Logger.error("Syncing of %s %s failed.", self._package, Logger.error("Syncing of %s %s failed.", self._package, str(self._version))
str(self._version))
sys.exit(1) sys.exit(1)
else: else:
# FIXME: Support this use case! # FIXME: Support this use case!
Logger.error("Uploading a synced package other than to Ubuntu " Logger.error("Uploading a synced package other than to Ubuntu is not supported yet!")
"is not supported yet!")
sys.exit(1) sys.exit(1)
return True return True

View File

@ -15,6 +15,7 @@
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
import logging
import os import os
import pwd import pwd
import shutil import shutil
@ -22,47 +23,43 @@ import subprocess
import sys import sys
from distro_info import UbuntuDistroInfo from distro_info import UbuntuDistroInfo
from launchpadlib.launchpad import Launchpad from launchpadlib.launchpad import Launchpad
from ubuntutools.update_maintainer import (update_maintainer,
MaintainerUpdateException)
from ubuntutools.question import input_number from ubuntutools.question import input_number
from ubuntutools.sponsor_patch.bugtask import BugTask, is_sync from ubuntutools.sponsor_patch.bugtask import BugTask, is_sync
from ubuntutools.sponsor_patch.patch import Patch from ubuntutools.sponsor_patch.patch import Patch
from ubuntutools.sponsor_patch.question import ask_for_manual_fixing from ubuntutools.sponsor_patch.question import ask_for_manual_fixing
from ubuntutools.sponsor_patch.source_package import SourcePackage from ubuntutools.sponsor_patch.source_package import SourcePackage
from ubuntutools.update_maintainer import MaintainerUpdateException, update_maintainer
import logging
Logger = logging.getLogger(__name__) Logger = logging.getLogger(__name__)
def is_command_available(command, check_sbin=False): def is_command_available(command, check_sbin=False):
"Is command in $PATH?" "Is command in $PATH?"
path = os.environ.get('PATH', '/usr/bin:/bin').split(':') path = os.environ.get("PATH", "/usr/bin:/bin").split(":")
if check_sbin: if check_sbin:
path += [directory[:-3] + 'sbin' path += [f"{directory[:-3]}sbin" for directory in path if directory.endswith("/bin")]
for directory in path if directory.endswith('/bin')] return any(os.access(os.path.join(directory, command), os.X_OK) for directory in path)
return any(os.access(os.path.join(directory, command), os.X_OK)
for directory in path)
def check_dependencies(): def check_dependencies():
"Do we have all the commands we need for full functionality?" "Do we have all the commands we need for full functionality?"
missing = [] missing = []
for cmd in ('patch', 'bzr', 'quilt', 'dput', 'lintian'): for cmd in ("patch", "bzr", "quilt", "dput", "lintian"):
if not is_command_available(cmd): if not is_command_available(cmd):
missing.append(cmd) missing.append(cmd)
if not is_command_available('bzr-buildpackage'): if not is_command_available("bzr-buildpackage"):
missing.append('bzr-builddeb') missing.append("bzr-builddeb")
if not any(is_command_available(cmd, check_sbin=True) if not any(
for cmd in ('pbuilder', 'sbuild', 'cowbuilder')): is_command_available(cmd, check_sbin=True) for cmd in ("pbuilder", "sbuild", "cowbuilder")
missing.append('pbuilder/cowbuilder/sbuild') ):
missing.append("pbuilder/cowbuilder/sbuild")
if missing: if missing:
Logger.warning("sponsor-patch requires %s to be installed for full " Logger.warning(
"functionality", ', '.join(missing)) "sponsor-patch requires %s to be installed for full functionality", ", ".join(missing)
)
def get_source_package_name(bug_task): def get_source_package_name(bug_task):
@ -84,15 +81,18 @@ def get_user_shell():
def edit_source(): def edit_source():
# Spawn shell to allow modifications # Spawn shell to allow modifications
cmd = [get_user_shell()] cmd = [get_user_shell()]
Logger.debug(' '.join(cmd)) Logger.debug(" ".join(cmd))
print("""An interactive shell was launched in print(
file://%s f"""An interactive shell was launched in
file://{os.getcwd()}
Edit your files. When you are done, exit the shell. If you wish to abort the Edit your files. When you are done, exit the shell. If you wish to abort the
process, exit the shell such that it returns an exit code other than zero. process, exit the shell such that it returns an exit code other than zero.
""" % (os.getcwd()), end=' ') """,
end=" ",
)
returncode = subprocess.call(cmd) returncode = subprocess.call(cmd)
if returncode != 0: if returncode != 0:
Logger.error("Shell exited with exit value %i." % (returncode)) Logger.error("Shell exited with exit value %i.", returncode)
sys.exit(1) sys.exit(1)
@ -100,30 +100,26 @@ def ask_for_patch_or_branch(bug, attached_patches, linked_branches):
patch = None patch = None
branch = None branch = None
if len(attached_patches) == 0: if len(attached_patches) == 0:
msg = "https://launchpad.net/bugs/%i has %i branches linked:" % \ msg = f"{len(linked_branches)} branches linked:"
(bug.id, len(linked_branches))
elif len(linked_branches) == 0: elif len(linked_branches) == 0:
msg = "https://launchpad.net/bugs/%i has %i patches attached:" % \ msg = f"{len(attached_patches)} patches attached:"
(bug.id, len(attached_patches))
else: else:
branches = "%i branch" % len(linked_branches) branches = f"{len(linked_branches)} branch"
if len(linked_branches) > 1: if len(linked_branches) > 1:
branches += "es" branches += "es"
patches = "%i patch" % len(attached_patches) patches = f"{len(attached_patches)} patch"
if len(attached_patches) > 1: if len(attached_patches) > 1:
patches += "es" patches += "es"
msg = "https://launchpad.net/bugs/%i has %s linked and %s attached:" % \ msg = f"{branches} linked and {patches} attached:"
(bug.id, branches, patches) Logger.info("https://launchpad.net/bugs/%i has %s", bug.id, msg)
Logger.info(msg)
i = 0 i = 0
for linked_branch in linked_branches: for linked_branch in linked_branches:
i += 1 i += 1
print("%i) %s" % (i, linked_branch.display_name)) print(f"{i}) {linked_branch.display_name}")
for attached_patch in attached_patches: for attached_patch in attached_patches:
i += 1 i += 1
print("%i) %s" % (i, attached_patch.title)) print(f"{i}) {attached_patch.title}")
selected = input_number("Which branch or patch do you want to download", selected = input_number("Which branch or patch do you want to download", 1, i, i)
1, i, i)
if selected <= len(linked_branches): if selected <= len(linked_branches):
branch = linked_branches[selected - 1].bzr_identity branch = linked_branches[selected - 1].bzr_identity
else: else:
@ -139,21 +135,26 @@ def get_patch_or_branch(bug):
linked_branches = [b.branch for b in bug.linked_branches] linked_branches = [b.branch for b in bug.linked_branches]
if len(attached_patches) == 0 and len(linked_branches) == 0: if len(attached_patches) == 0 and len(linked_branches) == 0:
if len(bug.attachments) == 0: if len(bug.attachments) == 0:
Logger.error("No attachment and no linked branch found on " Logger.error(
"bug #%i. Add the tag sync to the bug if it is " "No attachment and no linked branch found on "
"a sync request.", bug.id) "bug #%i. Add the tag sync to the bug if it is "
"a sync request.",
bug.id,
)
else: else:
Logger.error("No attached patch and no linked branch found. " Logger.error(
"Go to https://launchpad.net/bugs/%i and mark an " "No attached patch and no linked branch found. "
"attachment as patch.", bug.id) "Go to https://launchpad.net/bugs/%i and mark an "
"attachment as patch.",
bug.id,
)
sys.exit(1) sys.exit(1)
elif len(attached_patches) == 1 and len(linked_branches) == 0: elif len(attached_patches) == 1 and len(linked_branches) == 0:
patch = Patch(attached_patches[0]) patch = Patch(attached_patches[0])
elif len(attached_patches) == 0 and len(linked_branches) == 1: elif len(attached_patches) == 0 and len(linked_branches) == 1:
branch = linked_branches[0].bzr_identity branch = linked_branches[0].bzr_identity
else: else:
patch, branch = ask_for_patch_or_branch(bug, attached_patches, patch, branch = ask_for_patch_or_branch(bug, attached_patches, linked_branches)
linked_branches)
return (patch, branch) return (patch, branch)
@ -162,9 +163,9 @@ def download_branch(branch):
if os.path.isdir(dir_name): if os.path.isdir(dir_name):
shutil.rmtree(dir_name) shutil.rmtree(dir_name)
cmd = ["bzr", "branch", branch] cmd = ["bzr", "branch", branch]
Logger.debug(' '.join(cmd)) Logger.debug(" ".join(cmd))
if subprocess.call(cmd) != 0: if subprocess.call(cmd) != 0:
Logger.error("Failed to download branch %s." % (branch)) Logger.error("Failed to download branch %s.", branch)
sys.exit(1) sys.exit(1)
return dir_name return dir_name
@ -172,21 +173,21 @@ def download_branch(branch):
def merge_branch(branch): def merge_branch(branch):
edit = False edit = False
cmd = ["bzr", "merge", branch] cmd = ["bzr", "merge", branch]
Logger.debug(' '.join(cmd)) Logger.debug(" ".join(cmd))
if subprocess.call(cmd) != 0: if subprocess.call(cmd) != 0:
Logger.error("Failed to merge branch %s." % (branch)) Logger.error("Failed to merge branch %s.", branch)
ask_for_manual_fixing() ask_for_manual_fixing()
edit = True edit = True
return edit return edit
def extract_source(dsc_file, verbose=False): def extract_source(dsc_file, verbose=False):
cmd = ["dpkg-source", "--no-preparation", "-x", dsc_file] cmd = ["dpkg-source", "--skip-patches", "-x", dsc_file]
if not verbose: if not verbose:
cmd.insert(1, "-q") cmd.insert(1, "-q")
Logger.debug(' '.join(cmd)) Logger.debug(" ".join(cmd))
if subprocess.call(cmd) != 0: if subprocess.call(cmd) != 0:
Logger.error("Extraction of %s failed." % (os.path.basename(dsc_file))) Logger.error("Extraction of %s failed.", os.path.basename(dsc_file))
sys.exit(1) sys.exit(1)
@ -201,19 +202,18 @@ def get_open_ubuntu_bug_task(launchpad, bug, branch=None):
ubuntu_tasks = [x for x in bug_tasks if x.is_ubuntu_task()] ubuntu_tasks = [x for x in bug_tasks if x.is_ubuntu_task()]
bug_id = bug.id bug_id = bug.id
if branch: if branch:
branch = branch.split('/') branch = branch.split("/")
# Non-production LP? # Non-production LP?
if len(branch) > 5: if len(branch) > 5:
branch = branch[3:] branch = branch[3:]
if len(ubuntu_tasks) == 0: if len(ubuntu_tasks) == 0:
Logger.error("No Ubuntu bug task found on bug #%i." % (bug_id)) Logger.error("No Ubuntu bug task found on bug #%i.", bug_id)
sys.exit(1) sys.exit(1)
elif len(ubuntu_tasks) == 1: elif len(ubuntu_tasks) == 1:
task = ubuntu_tasks[0] task = ubuntu_tasks[0]
if len(ubuntu_tasks) > 1 and branch and branch[1] == 'ubuntu': if len(ubuntu_tasks) > 1 and branch and branch[1] == "ubuntu":
tasks = [t for t in ubuntu_tasks if tasks = [t for t in ubuntu_tasks if t.get_series() == branch[2] and t.package == branch[3]]
t.get_series() == branch[2] and t.package == branch[3]]
if len(tasks) > 1: if len(tasks) > 1:
# A bug targeted to the development series? # A bug targeted to the development series?
tasks = [t for t in tasks if t.series is not None] tasks = [t for t in tasks if t.series is not None]
@ -221,21 +221,26 @@ def get_open_ubuntu_bug_task(launchpad, bug, branch=None):
task = tasks[0] task = tasks[0]
elif len(ubuntu_tasks) > 1: elif len(ubuntu_tasks) > 1:
task_list = [t.get_short_info() for t in ubuntu_tasks] task_list = [t.get_short_info() for t in ubuntu_tasks]
Logger.debug("%i Ubuntu tasks exist for bug #%i.\n%s", len(ubuntu_tasks), Logger.debug(
bug_id, "\n".join(task_list)) "%i Ubuntu tasks exist for bug #%i.\n%s",
len(ubuntu_tasks),
bug_id,
"\n".join(task_list),
)
open_ubuntu_tasks = [x for x in ubuntu_tasks if not x.is_complete()] open_ubuntu_tasks = [x for x in ubuntu_tasks if not x.is_complete()]
if len(open_ubuntu_tasks) == 1: if len(open_ubuntu_tasks) == 1:
task = open_ubuntu_tasks[0] task = open_ubuntu_tasks[0]
else: else:
Logger.info("https://launchpad.net/bugs/%i has %i Ubuntu tasks:" % Logger.info(
(bug_id, len(ubuntu_tasks))) "https://launchpad.net/bugs/%i has %i Ubuntu tasks:", bug_id, len(ubuntu_tasks)
for i in range(len(ubuntu_tasks)): )
print("%i) %s" % (i + 1, for i, ubuntu_task in enumerate(ubuntu_tasks):
ubuntu_tasks[i].get_package_and_series())) print(f"{i + 1}) {ubuntu_task.get_package_and_series()}")
selected = input_number("To which Ubuntu task does the patch belong", selected = input_number(
1, len(ubuntu_tasks)) "To which Ubuntu task does the patch belong", 1, len(ubuntu_tasks)
)
task = ubuntu_tasks[selected - 1] task = ubuntu_tasks[selected - 1]
Logger.debug("Selected Ubuntu task: %s" % (task.get_short_info())) Logger.debug("Selected Ubuntu task: %s", task.get_short_info())
return task return task
@ -246,11 +251,15 @@ def _create_and_change_into(workdir):
try: try:
os.makedirs(workdir) os.makedirs(workdir)
except os.error as error: except os.error as error:
Logger.error("Failed to create the working directory %s [Errno %i]: %s." % Logger.error(
(workdir, error.errno, error.strerror)) "Failed to create the working directory %s [Errno %i]: %s.",
workdir,
error.errno,
error.strerror,
)
sys.exit(1) sys.exit(1)
if workdir != os.getcwd(): if workdir != os.getcwd():
Logger.debug("cd " + workdir) Logger.debug("cd %s", workdir)
os.chdir(workdir) os.chdir(workdir)
@ -267,7 +276,7 @@ def _update_maintainer_field():
def _update_timestamp(): def _update_timestamp():
"""Run dch to update the timestamp of debian/changelog.""" """Run dch to update the timestamp of debian/changelog."""
cmd = ["dch", "--maintmaint", "--release", ""] cmd = ["dch", "--maintmaint", "--release", ""]
Logger.debug(' '.join(cmd)) Logger.debug(" ".join(cmd))
if subprocess.call(cmd) != 0: if subprocess.call(cmd) != 0:
Logger.debug("Failed to update timestamp in debian/changelog.") Logger.debug("Failed to update timestamp in debian/changelog.")
@ -279,13 +288,13 @@ def _download_and_change_into(task, dsc_file, patch, branch):
branch_dir = download_branch(task.get_branch_link()) branch_dir = download_branch(task.get_branch_link())
# change directory # change directory
Logger.debug("cd " + branch_dir) Logger.debug("cd %s", branch_dir)
os.chdir(branch_dir) os.chdir(branch_dir)
else: else:
if patch: if patch:
patch.download() patch.download()
Logger.debug("Ubuntu package: %s" % (task.package)) Logger.debug("Ubuntu package: %s", task.package)
if task.is_merge(): if task.is_merge():
Logger.debug("The task is a merge request.") Logger.debug("The task is a merge request.")
if task.is_sync(): if task.is_sync():
@ -294,13 +303,12 @@ def _download_and_change_into(task, dsc_file, patch, branch):
extract_source(dsc_file, Logger.isEnabledFor(logging.DEBUG)) extract_source(dsc_file, Logger.isEnabledFor(logging.DEBUG))
# change directory # change directory
directory = task.package + '-' + task.get_version().upstream_version directory = f"{task.package}-{task.get_version().upstream_version}"
Logger.debug("cd " + directory) Logger.debug("cd %s", directory)
os.chdir(directory) os.chdir(directory)
def sponsor_patch(bug_number, build, builder, edit, keyid, lpinstance, update, def sponsor_patch(bug_number, build, builder, edit, keyid, lpinstance, update, upload, workdir):
upload, workdir):
workdir = os.path.realpath(os.path.expanduser(workdir)) workdir = os.path.realpath(os.path.expanduser(workdir))
_create_and_change_into(workdir) _create_and_change_into(workdir)
@ -331,17 +339,13 @@ def sponsor_patch(bug_number, build, builder, edit, keyid, lpinstance, update,
update = False update = False
else: else:
# We are going to run lintian, so we need a source package # We are going to run lintian, so we need a source package
successful = source_package.build_source(None, upload, successful = source_package.build_source(None, upload, previous_version)
previous_version)
if successful: if successful:
series = task.get_debian_source_series() series = task.get_debian_source_series()
if source_package.sync(upload, series, bug_number, bug.owner.name): if source_package.sync(upload, series, bug_number, bug.owner.name):
return return
else: edit = True
edit = True
else:
edit = True
if patch: if patch:
edit |= patch.apply(task) edit |= patch.apply(task)
@ -363,8 +367,7 @@ def sponsor_patch(bug_number, build, builder, edit, keyid, lpinstance, update,
_update_timestamp() _update_timestamp()
if not source_package.build_source(keyid, upload, if not source_package.build_source(keyid, upload, task.get_previous_version()):
task.get_previous_version()):
continue continue
source_package.generate_debdiff(dsc_file) source_package.generate_debdiff(dsc_file)

View File

@ -17,66 +17,76 @@
import os import os
import subprocess import subprocess
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from ubuntutools.version import Version from ubuntutools.version import Version
class ExamplePackage(object): class ExamplePackage:
def __init__(self, source='example', version='1.0-1', destdir='test-data'): def __init__(self, source="example", version="1.0-1", destdir="test-data"):
self.source = source self.source = source
self.version = Version(version) self.version = Version(version)
self.destdir = Path(destdir) self.destdir = Path(destdir)
self.env = dict(os.environ) self.env = dict(os.environ)
self.env['DEBFULLNAME'] = 'Example' self.env["DEBFULLNAME"] = "Example"
self.env['DEBEMAIL'] = 'example@example.net' self.env["DEBEMAIL"] = "example@example.net"
@property @property
def orig(self): def orig(self):
return self.destdir / f'{self.source}_{self.version.upstream_version}.orig.tar.xz' return self.destdir / f"{self.source}_{self.version.upstream_version}.orig.tar.xz"
@property @property
def debian(self): def debian(self):
return self.destdir / f'{self.source}_{self.version}.debian.tar.xz' return self.destdir / f"{self.source}_{self.version}.debian.tar.xz"
@property @property
def dsc(self): def dsc(self):
return self.destdir / f'{self.source}_{self.version}.dsc' return self.destdir / f"{self.source}_{self.version}.dsc"
@property @property
def dirname(self): def dirname(self):
return f'{self.source}-{self.version.upstream_version}' return f"{self.source}-{self.version.upstream_version}"
@property @property
def content_filename(self): def content_filename(self):
return 'content' return "content"
@property @property
def content_text(self): def content_text(self):
return 'my content' return "my content"
def create(self): def create(self):
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as tmpdir:
self._create(Path(d)) self._create(Path(tmpdir))
def _create(self, d): def _create(self, directory: Path):
pkgdir = d / self.dirname pkgdir = directory / self.dirname
pkgdir.mkdir() pkgdir.mkdir()
(pkgdir / self.content_filename).write_text(self.content_text) (pkgdir / self.content_filename).write_text(self.content_text)
# run dh_make to create orig tarball # run dh_make to create orig tarball
subprocess.run('dh_make -sy --createorig'.split(), subprocess.run(
check=True, env=self.env, cwd=str(pkgdir), "dh_make -sy --createorig".split(),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) check=True,
env=self.env,
cwd=str(pkgdir),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
# run dpkg-source -b to create debian tar and dsc # run dpkg-source -b to create debian tar and dsc
subprocess.run(f'dpkg-source -b {self.dirname}'.split(), subprocess.run(
check=True, env=self.env, cwd=str(d), f"dpkg-source -b {self.dirname}".split(),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) check=True,
env=self.env,
cwd=str(directory),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
# move tarballs and dsc to destdir # move tarballs and dsc to destdir
self.destdir.mkdir(parents=True, exist_ok=True) self.destdir.mkdir(parents=True, exist_ok=True)
(d / self.orig.name).rename(self.orig) (directory / self.orig.name).rename(self.orig)
(d / self.debian.name).rename(self.debian) (directory / self.debian.name).rename(self.debian)
(d / self.dsc.name).rename(self.dsc) (directory / self.dsc.name).rename(self.dsc)

View File

@ -18,19 +18,17 @@
import filecmp import filecmp
import tempfile import tempfile
import unittest import unittest
import ubuntutools.archive
from pathlib import Path from pathlib import Path
import ubuntutools.archive
from ubuntutools.test.example_package import ExamplePackage from ubuntutools.test.example_package import ExamplePackage
class BaseVerificationTestCase(unittest.TestCase): class BaseVerificationTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
d = tempfile.TemporaryDirectory() tmpdir = tempfile.TemporaryDirectory()
self.addCleanup(d.cleanup) self.addCleanup(tmpdir.cleanup)
self.pkg = ExamplePackage(destdir=Path(d.name)) self.pkg = ExamplePackage(destdir=Path(tmpdir.name))
self.pkg.create() self.pkg.create()
self.dsc = ubuntutools.archive.Dsc(self.pkg.dsc.read_bytes()) self.dsc = ubuntutools.archive.Dsc(self.pkg.dsc.read_bytes())
@ -41,7 +39,7 @@ class DscVerificationTestCase(BaseVerificationTestCase):
self.assertTrue(self.dsc.verify_file(self.pkg.debian)) self.assertTrue(self.dsc.verify_file(self.pkg.debian))
def test_missing(self): def test_missing(self):
self.assertFalse(self.dsc.verify_file(self.pkg.destdir / 'does.not.exist')) self.assertFalse(self.dsc.verify_file(self.pkg.destdir / "does.not.exist"))
def test_bad(self): def test_bad(self):
data = self.pkg.orig.read_bytes() data = self.pkg.orig.read_bytes()
@ -51,13 +49,13 @@ class DscVerificationTestCase(BaseVerificationTestCase):
self.assertFalse(self.dsc.verify_file(self.pkg.orig)) self.assertFalse(self.dsc.verify_file(self.pkg.orig))
def test_sha1(self): def test_sha1(self):
del self.dsc['Checksums-Sha256'] del self.dsc["Checksums-Sha256"]
self.test_good() self.test_good()
self.test_bad() self.test_bad()
def test_md5(self): def test_md5(self):
del self.dsc['Checksums-Sha256'] del self.dsc["Checksums-Sha256"]
del self.dsc['Checksums-Sha1'] del self.dsc["Checksums-Sha1"]
self.test_good() self.test_good()
self.test_bad() self.test_bad()
@ -67,17 +65,17 @@ class LocalSourcePackageTestCase(BaseVerificationTestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
d = tempfile.TemporaryDirectory() tmpdir = tempfile.TemporaryDirectory()
self.addCleanup(d.cleanup) self.addCleanup(tmpdir.cleanup)
self.workdir = Path(d.name) self.workdir = Path(tmpdir.name)
def pull(self, **kwargs): def pull(self, **kwargs):
''' Do the pull from pkg dir to the workdir, return the SourcePackage ''' """Do the pull from pkg dir to the workdir, return the SourcePackage"""
srcpkg = self.SourcePackage(dscfile=self.pkg.dsc, workdir=self.workdir, **kwargs) srcpkg = self.SourcePackage(dscfile=self.pkg.dsc, workdir=self.workdir, **kwargs)
srcpkg.pull() srcpkg.pull()
return srcpkg return srcpkg
def test_pull(self, **kwargs): def _test_pull(self, **kwargs):
srcpkg = self.pull(**kwargs) srcpkg = self.pull(**kwargs)
self.assertTrue(filecmp.cmp(self.pkg.dsc, self.workdir / self.pkg.dsc.name)) self.assertTrue(filecmp.cmp(self.pkg.dsc, self.workdir / self.pkg.dsc.name))
self.assertTrue(filecmp.cmp(self.pkg.orig, self.workdir / self.pkg.orig.name)) self.assertTrue(filecmp.cmp(self.pkg.orig, self.workdir / self.pkg.orig.name))
@ -85,16 +83,16 @@ class LocalSourcePackageTestCase(BaseVerificationTestCase):
return srcpkg return srcpkg
def test_unpack(self, **kwargs): def test_unpack(self, **kwargs):
srcpkg = kwargs.get('srcpkg', self.pull(**kwargs)) srcpkg = kwargs.get("srcpkg", self.pull(**kwargs))
srcpkg.unpack() srcpkg.unpack()
content = self.workdir / self.pkg.dirname / self.pkg.content_filename content = self.workdir / self.pkg.dirname / self.pkg.content_filename
self.assertEqual(self.pkg.content_text, content.read_text()) self.assertEqual(self.pkg.content_text, content.read_text())
debian = self.workdir / self.pkg.dirname / 'debian' debian = self.workdir / self.pkg.dirname / "debian"
self.assertTrue(debian.exists()) self.assertTrue(debian.exists())
self.assertTrue(debian.is_dir()) self.assertTrue(debian.is_dir())
def test_pull_and_unpack(self, **kwargs): def test_pull_and_unpack(self, **kwargs):
self.test_unpack(srcpkg=self.test_pull(**kwargs)) self.test_unpack(srcpkg=self._test_pull(**kwargs))
def test_with_package(self): def test_with_package(self):
self.test_pull_and_unpack(package=self.pkg.source) self.test_pull_and_unpack(package=self.pkg.source)
@ -103,12 +101,12 @@ class LocalSourcePackageTestCase(BaseVerificationTestCase):
self.test_pull_and_unpack(package=self.pkg.source, version=self.pkg.version) self.test_pull_and_unpack(package=self.pkg.source, version=self.pkg.version)
def test_with_package_version_component(self): def test_with_package_version_component(self):
self.test_pull_and_unpack(package=self.pkg.source, self.test_pull_and_unpack(
version=self.pkg.version, package=self.pkg.source, version=self.pkg.version, componet="main"
componet='main') )
def test_verification(self): def test_verification(self):
corruption = b'CORRUPTION' corruption = b"CORRUPTION"
self.pull() self.pull()
@ -119,7 +117,7 @@ class LocalSourcePackageTestCase(BaseVerificationTestCase):
testfile.write_bytes(corruption) testfile.write_bytes(corruption)
self.assertEqual(testfile.read_bytes(), corruption) self.assertEqual(testfile.read_bytes(), corruption)
self.test_pull() self._test_pull()
self.assertTrue(testfile.exists()) self.assertTrue(testfile.exists())
self.assertTrue(testfile.is_file()) self.assertTrue(testfile.is_file())
self.assertNotEqual(testfile.read_bytes(), corruption) self.assertNotEqual(testfile.read_bytes(), corruption)

View File

@ -17,9 +17,7 @@
import locale import locale
import os import os
# import sys
import unittest import unittest
from io import StringIO from io import StringIO
from unittest import mock from unittest import mock
@ -27,27 +25,25 @@ from ubuntutools.config import UDTConfig, ubu_email
class ConfigTestCase(unittest.TestCase): class ConfigTestCase(unittest.TestCase):
_config_files = { _config_files = {"system": "", "user": ""}
'system': '',
'user': '',
}
def _fake_open(self, filename, mode='r'): def _fake_open(self, filename, mode="r", encoding=None):
if mode != 'r': self.assertTrue(encoding, f"encoding for {filename} not specified")
if mode != "r":
raise IOError("Read only fake-file") raise IOError("Read only fake-file")
files = { files = {
'/etc/devscripts.conf': self._config_files['system'], "/etc/devscripts.conf": self._config_files["system"],
os.path.expanduser('~/.devscripts'): self._config_files['user'], os.path.expanduser("~/.devscripts"): self._config_files["user"],
} }
if filename not in files: if filename not in files:
raise IOError("No such file or directory: '%s'" % filename) raise IOError(f"No such file or directory: '{filename}'")
return StringIO(files[filename]) return StringIO(files[filename])
def setUp(self): def setUp(self):
super(ConfigTestCase, self).setUp() super().setUp()
m = mock.mock_open() open_mock = mock.mock_open()
m.side_effect = self._fake_open open_mock.side_effect = self._fake_open
patcher = mock.patch('builtins.open', m) patcher = mock.patch("builtins.open", open_mock)
self.addCleanup(patcher.stop) self.addCleanup(patcher.stop)
patcher.start() patcher.start()
@ -65,14 +61,16 @@ class ConfigTestCase(unittest.TestCase):
self.clean_environment() self.clean_environment()
def clean_environment(self): def clean_environment(self):
self._config_files['system'] = '' self._config_files["system"] = ""
self._config_files['user'] = '' self._config_files["user"] = ""
for k in list(os.environ.keys()): for k in list(os.environ.keys()):
if k.startswith(('UBUNTUTOOLS_', 'TEST_')): if k.startswith(("UBUNTUTOOLS_", "TEST_")):
del os.environ[k] del os.environ[k]
def test_config_parsing(self): def test_config_parsing(self):
self._config_files['user'] = """#COMMENT=yes self._config_files[
"user"
] = """#COMMENT=yes
\tTAB_INDENTED=yes \tTAB_INDENTED=yes
SPACE_INDENTED=yes SPACE_INDENTED=yes
SPACE_SUFFIX=yes SPACE_SUFFIX=yes
@ -85,59 +83,64 @@ INHERIT=user
REPEAT=no REPEAT=no
REPEAT=yes REPEAT=yes
""" """
self._config_files['system'] = 'INHERIT=system' self._config_files["system"] = "INHERIT=system"
self.assertEqual(UDTConfig(prefix='TEST').config, { self.assertEqual(
'TAB_INDENTED': 'yes', UDTConfig(prefix="TEST").config,
'SPACE_INDENTED': 'yes', {
'SPACE_SUFFIX': 'yes', "TAB_INDENTED": "yes",
'SINGLE_QUOTE': 'yes no', "SPACE_INDENTED": "yes",
'DOUBLE_QUOTE': 'yes no', "SPACE_SUFFIX": "yes",
'QUOTED_QUOTE': "it's", "SINGLE_QUOTE": "yes no",
'PAIR_QUOTES': 'yes a no', "DOUBLE_QUOTE": "yes no",
'COMMAND_EXECUTION': 'a', "QUOTED_QUOTE": "it's",
'INHERIT': 'user', "PAIR_QUOTES": "yes a no",
'REPEAT': 'yes', "COMMAND_EXECUTION": "a",
}) "INHERIT": "user",
"REPEAT": "yes",
},
)
# errs = Logger.stderr.getvalue().strip() # errs = Logger.stderr.getvalue().strip()
# Logger.stderr = StringIO() # Logger.stderr = StringIO()
# self.assertEqual(len(errs.splitlines()), 1) # self.assertEqual(len(errs.splitlines()), 1)
# self.assertRegex(errs, # self.assertRegex(errs,
# r'Warning: Cannot parse.*\bCOMMAND_EXECUTION=a') # r'Warning: Cannot parse.*\bCOMMAND_EXECUTION=a')
def get_value(self, *args, **kwargs): @staticmethod
config = UDTConfig(prefix='TEST') def get_value(*args, **kwargs):
config = UDTConfig(prefix="TEST")
return config.get_value(*args, **kwargs) return config.get_value(*args, **kwargs)
def test_defaults(self): def test_defaults(self):
self.assertEqual(self.get_value('BUILDER'), 'pbuilder') self.assertEqual(self.get_value("BUILDER"), "pbuilder")
def test_provided_default(self): def test_provided_default(self):
self.assertEqual(self.get_value('BUILDER', default='foo'), 'foo') self.assertEqual(self.get_value("BUILDER", default="foo"), "foo")
def test_scriptname_precedence(self): def test_scriptname_precedence(self):
self._config_files['user'] = """TEST_BUILDER=foo self._config_files[
"user"
] = """TEST_BUILDER=foo
UBUNTUTOOLS_BUILDER=bar""" UBUNTUTOOLS_BUILDER=bar"""
self.assertEqual(self.get_value('BUILDER'), 'foo') self.assertEqual(self.get_value("BUILDER"), "foo")
def test_configfile_precedence(self): def test_configfile_precedence(self):
self._config_files['system'] = "UBUNTUTOOLS_BUILDER=foo" self._config_files["system"] = "UBUNTUTOOLS_BUILDER=foo"
self._config_files['user'] = "UBUNTUTOOLS_BUILDER=bar" self._config_files["user"] = "UBUNTUTOOLS_BUILDER=bar"
self.assertEqual(self.get_value('BUILDER'), 'bar') self.assertEqual(self.get_value("BUILDER"), "bar")
def test_environment_precedence(self): def test_environment_precedence(self):
self._config_files['user'] = "UBUNTUTOOLS_BUILDER=bar" self._config_files["user"] = "UBUNTUTOOLS_BUILDER=bar"
os.environ['UBUNTUTOOLS_BUILDER'] = 'baz' os.environ["UBUNTUTOOLS_BUILDER"] = "baz"
self.assertEqual(self.get_value('BUILDER'), 'baz') self.assertEqual(self.get_value("BUILDER"), "baz")
def test_general_environment_specific_config_precedence(self): def test_general_environment_specific_config_precedence(self):
self._config_files['user'] = "TEST_BUILDER=bar" self._config_files["user"] = "TEST_BUILDER=bar"
os.environ['UBUNTUTOOLS_BUILDER'] = 'foo' os.environ["UBUNTUTOOLS_BUILDER"] = "foo"
self.assertEqual(self.get_value('BUILDER'), 'bar') self.assertEqual(self.get_value("BUILDER"), "bar")
def test_compat_keys(self): def test_compat_keys(self):
self._config_files['user'] = 'COMPATFOOBAR=bar' self._config_files["user"] = "COMPATFOOBAR=bar"
self.assertEqual(self.get_value('QUX', compat_keys=['COMPATFOOBAR']), self.assertEqual(self.get_value("QUX", compat_keys=["COMPATFOOBAR"]), "bar")
'bar')
# errs = Logger.stderr.getvalue().strip() # errs = Logger.stderr.getvalue().strip()
# Logger.stderr = StringIO() # Logger.stderr = StringIO()
# self.assertEqual(len(errs.splitlines()), 1) # self.assertEqual(len(errs.splitlines()), 1)
@ -145,16 +148,16 @@ REPEAT=yes
# r'deprecated.*\bCOMPATFOOBAR\b.*\bTEST_QUX\b') # r'deprecated.*\bCOMPATFOOBAR\b.*\bTEST_QUX\b')
def test_boolean(self): def test_boolean(self):
self._config_files['user'] = "TEST_BOOLEAN=yes" self._config_files["user"] = "TEST_BOOLEAN=yes"
self.assertEqual(self.get_value('BOOLEAN', boolean=True), True) self.assertEqual(self.get_value("BOOLEAN", boolean=True), True)
self._config_files['user'] = "TEST_BOOLEAN=no" self._config_files["user"] = "TEST_BOOLEAN=no"
self.assertEqual(self.get_value('BOOLEAN', boolean=True), False) self.assertEqual(self.get_value("BOOLEAN", boolean=True), False)
self._config_files['user'] = "TEST_BOOLEAN=true" self._config_files["user"] = "TEST_BOOLEAN=true"
self.assertEqual(self.get_value('BOOLEAN', boolean=True), None) self.assertEqual(self.get_value("BOOLEAN", boolean=True), None)
def test_nonpackagewide(self): def test_nonpackagewide(self):
self._config_files['user'] = 'UBUNTUTOOLS_FOOBAR=a' self._config_files["user"] = "UBUNTUTOOLS_FOOBAR=a"
self.assertEqual(self.get_value('FOOBAR'), None) self.assertEqual(self.get_value("FOOBAR"), None)
class UbuEmailTestCase(unittest.TestCase): class UbuEmailTestCase(unittest.TestCase):
@ -164,72 +167,72 @@ class UbuEmailTestCase(unittest.TestCase):
def tearDown(self): def tearDown(self):
self.clean_environment() self.clean_environment()
def clean_environment(self): @staticmethod
for k in ('UBUMAIL', 'DEBEMAIL', 'DEBFULLNAME'): def clean_environment():
for k in ("UBUMAIL", "DEBEMAIL", "DEBFULLNAME"):
if k in os.environ: if k in os.environ:
del os.environ[k] del os.environ[k]
def test_pristine(self): def test_pristine(self):
os.environ['DEBFULLNAME'] = name = 'Joe Developer' os.environ["DEBFULLNAME"] = name = "Joe Developer"
os.environ['DEBEMAIL'] = email = 'joe@example.net' os.environ["DEBEMAIL"] = email = "joe@example.net"
self.assertEqual(ubu_email(), (name, email)) self.assertEqual(ubu_email(), (name, email))
def test_two_hat(self): def test_two_hat(self):
os.environ['DEBFULLNAME'] = name = 'Joe Developer' os.environ["DEBFULLNAME"] = name = "Joe Developer"
os.environ['DEBEMAIL'] = 'joe@debian.org' os.environ["DEBEMAIL"] = "joe@debian.org"
os.environ['UBUMAIL'] = email = 'joe@ubuntu.com' os.environ["UBUMAIL"] = email = "joe@ubuntu.com"
self.assertEqual(ubu_email(), (name, email)) self.assertEqual(ubu_email(), (name, email))
self.assertEqual(os.environ['DEBFULLNAME'], name) self.assertEqual(os.environ["DEBFULLNAME"], name)
self.assertEqual(os.environ['DEBEMAIL'], email) self.assertEqual(os.environ["DEBEMAIL"], email)
def test_two_hat_cmdlineoverride(self): def test_two_hat_cmdlineoverride(self):
os.environ['DEBFULLNAME'] = 'Joe Developer' os.environ["DEBFULLNAME"] = "Joe Developer"
os.environ['DEBEMAIL'] = 'joe@debian.org' os.environ["DEBEMAIL"] = "joe@debian.org"
os.environ['UBUMAIL'] = 'joe@ubuntu.com' os.environ["UBUMAIL"] = "joe@ubuntu.com"
name = 'Foo Bar' name = "Foo Bar"
email = 'joe@example.net' email = "joe@example.net"
self.assertEqual(ubu_email(name, email), (name, email)) self.assertEqual(ubu_email(name, email), (name, email))
self.assertEqual(os.environ['DEBFULLNAME'], name) self.assertEqual(os.environ["DEBFULLNAME"], name)
self.assertEqual(os.environ['DEBEMAIL'], email) self.assertEqual(os.environ["DEBEMAIL"], email)
def test_two_hat_noexport(self): def test_two_hat_noexport(self):
os.environ['DEBFULLNAME'] = name = 'Joe Developer' os.environ["DEBFULLNAME"] = name = "Joe Developer"
os.environ['DEBEMAIL'] = demail = 'joe@debian.org' os.environ["DEBEMAIL"] = demail = "joe@debian.org"
os.environ['UBUMAIL'] = uemail = 'joe@ubuntu.com' os.environ["UBUMAIL"] = uemail = "joe@ubuntu.com"
self.assertEqual(ubu_email(export=False), (name, uemail)) self.assertEqual(ubu_email(export=False), (name, uemail))
self.assertEqual(os.environ['DEBFULLNAME'], name) self.assertEqual(os.environ["DEBFULLNAME"], name)
self.assertEqual(os.environ['DEBEMAIL'], demail) self.assertEqual(os.environ["DEBEMAIL"], demail)
def test_two_hat_with_name(self): def test_two_hat_with_name(self):
os.environ['DEBFULLNAME'] = 'Joe Developer' os.environ["DEBFULLNAME"] = "Joe Developer"
os.environ['DEBEMAIL'] = 'joe@debian.org' os.environ["DEBEMAIL"] = "joe@debian.org"
name = 'Joe Ubuntunista' name = "Joe Ubuntunista"
email = 'joe@ubuntu.com' email = "joe@ubuntu.com"
os.environ['UBUMAIL'] = '%s <%s>' % (name, email) os.environ["UBUMAIL"] = f"{name} <{email}>"
self.assertEqual(ubu_email(), (name, email)) self.assertEqual(ubu_email(), (name, email))
self.assertEqual(os.environ['DEBFULLNAME'], name) self.assertEqual(os.environ["DEBFULLNAME"], name)
self.assertEqual(os.environ['DEBEMAIL'], email) self.assertEqual(os.environ["DEBEMAIL"], email)
def test_debemail_with_name(self): def test_debemail_with_name(self):
name = 'Joe Developer' name = "Joe Developer"
email = 'joe@example.net' email = "joe@example.net"
os.environ['DEBEMAIL'] = orig = '%s <%s>' % (name, email) os.environ["DEBEMAIL"] = orig = f"{name} <{email}>"
self.assertEqual(ubu_email(), (name, email)) self.assertEqual(ubu_email(), (name, email))
self.assertEqual(os.environ['DEBEMAIL'], orig) self.assertEqual(os.environ["DEBEMAIL"], orig)
def test_unicode_name(self): def test_unicode_name(self):
encoding = locale.getdefaultlocale()[1] encoding = locale.getlocale()[1]
if not encoding: if not encoding:
encoding = 'utf-8' encoding = "utf-8"
name = 'Jöe Déveloper' name = "Jöe Déveloper"
env_name = name env_name = name
if isinstance(name, bytes): if isinstance(name, bytes):
name = 'Jöe Déveloper'.decode('utf-8') name = "Jöe Déveloper".decode("utf-8")
env_name = name.encode(encoding) env_name = name.encode(encoding)
try: try:
os.environ['DEBFULLNAME'] = env_name os.environ["DEBFULLNAME"] = env_name
except UnicodeEncodeError: except UnicodeEncodeError:
raise unittest.SkipTest("python interpreter is not running in an " self.skipTest("python interpreter is not running in an unicode capable locale")
"unicode capable locale") os.environ["DEBEMAIL"] = email = "joe@example.net"
os.environ['DEBEMAIL'] = email = 'joe@example.net'
self.assertEqual(ubu_email(), (name, email)) self.assertEqual(ubu_email(), (name, email))

View File

@ -19,7 +19,6 @@ import unittest
from setup import scripts from setup import scripts
TIMEOUT = 10 TIMEOUT = 10
@ -27,10 +26,12 @@ class HelpTestCase(unittest.TestCase):
def test_script(self): def test_script(self):
for script in scripts: for script in scripts:
with self.subTest(script=script): with self.subTest(script=script):
result = subprocess.run([f'./{script}', '--help'], result = subprocess.run(
encoding='UTF-8', [f"./{script}", "--help"],
timeout=10, encoding="UTF-8",
check=True, timeout=10,
stdout=subprocess.PIPE, check=True,
stderr=subprocess.PIPE) stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
self.assertFalse(result.stderr.strip()) self.assertFalse(result.stderr.strip())

View File

@ -0,0 +1,33 @@
# Copyright (C) 2024 Canonical Ltd.
# Author: Chris Peterson <chris.peterson@canonical.com>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
import unittest
# Binary Tests
class BinaryTests(unittest.TestCase):
# The requestsync binary has the option of using the launchpad api
# to log in but requires python3-keyring in addition to
# python3-launchpadlib. Testing the integrated login functionality
# automatically isn't very feasbile, but we can at least write a smoke
# test to make sure the required packages are installed.
# See LP: #2049217
def test_keyring_installed(self):
"""Smoke test for required lp api dependencies"""
try:
import keyring # noqa: F401
except ModuleNotFoundError:
raise ModuleNotFoundError("package python3-keyring is not installed")

View File

@ -0,0 +1,128 @@
# Copyright (C) 2024 Canonical Ltd.
# Author: Chris Peterson <chris.peterson@canonical.com>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
""" Tests for running_autopkgtests
Tests using cached data from autopkgtest servers.
These tests only ensure code changes don't change parsing behavior
of the response data. If the response format changes, then the cached
responses will need to change as well.
"""
import unittest
from unittest.mock import patch
from ubuntutools.running_autopkgtests import (
URL_QUEUED,
URL_RUNNING,
_get_jobs,
get_queued,
get_running,
)
# Cached binary response data from autopkgtest server
RUN_DATA = (
b'{"pyatem": {'
b" \"submit-time_2024-01-19 19:37:36;triggers_['python3-defaults/3.12.1-0ubuntu1'];\":"
b' {"noble": {"arm64": [{"triggers": ["python3-defaults/3.12.1-0ubuntu1"],'
b' "submit-time": "2024-01-19 19:37:36"}, 380, "<omitted log>"]}}}}'
)
QUEUED_DATA = (
b'{"ubuntu": {"noble": {"arm64": ["libobject-accessor-perl {\\"requester\\": \\"someone\\",'
b' \\"submit-time\\": \\"2024-01-18 01:08:55\\",'
b' \\"triggers\\": [\\"perl/5.38.2-3\\", \\"liblocale-gettext-perl/1.07-6build1\\"]}"]}}}'
)
# Expected result(s) of parsing the above JSON data
RUNNING_JOB = {
"pyatem": {
"submit-time_2024-01-19 19:37:36;triggers_['python3-defaults/3.12.1-0ubuntu1'];": {
"noble": {
"arm64": [
{
"triggers": ["python3-defaults/3.12.1-0ubuntu1"],
"submit-time": "2024-01-19 19:37:36",
},
380,
"<omitted log>",
]
}
}
}
}
QUEUED_JOB = {
"ubuntu": {
"noble": {
"arm64": [
'libobject-accessor-perl {"requester": "someone",'
' "submit-time": "2024-01-18 01:08:55",'
' "triggers": ["perl/5.38.2-3", "liblocale-gettext-perl/1.07-6build1"]}'
]
}
}
}
PRIVATE_JOB = {"ppa": {"noble": {"arm64": ["private job"]}}}
# Expected textual output of the program based on the above data
RUNNING_OUTPUT = (
"R 0:06:20 pyatem - noble arm64"
" - python3-defaults/3.12.1-0ubuntu1 -\n"
)
QUEUED_OUTPUT = (
"Q0001 -:-- libobject-accessor-perl ubuntu noble arm64"
" - perl/5.38.2-3,liblocale-gettext-perl/1.07-6build1\n"
)
PRIVATE_OUTPUT = (
"Q0001 -:-- private job ppa noble arm64"
" private job private job\n"
)
class RunningAutopkgtestTestCase(unittest.TestCase):
"""Assert helper functions parse data correctly"""
maxDiff = None
@patch("urllib.request.urlopen")
def test_get_running_jobs(self, mock_response):
"""Test: Correctly parse autopkgtest json data for running tests"""
mock_response.return_value.__enter__.return_value.read.return_value = RUN_DATA
jobs = _get_jobs(URL_RUNNING)
self.assertEqual(RUNNING_JOB, jobs)
@patch("urllib.request.urlopen")
def test_get_queued_jobs(self, mock_response):
"""Test: Correctly parse autopkgtest json data for queued tests"""
mock_response.return_value.__enter__.return_value.read.return_value = QUEUED_DATA
jobs = _get_jobs(URL_QUEUED)
self.assertEqual(QUEUED_JOB, jobs)
def test_get_running_output(self):
"""Test: Correctly print running tests"""
with patch("ubuntutools.running_autopkgtests._get_jobs", return_value=RUNNING_JOB):
self.assertEqual(get_running(), RUNNING_OUTPUT)
def test_get_queued_output(self):
"""Test: Correctly print queued tests"""
with patch("ubuntutools.running_autopkgtests._get_jobs", return_value=QUEUED_JOB):
self.assertEqual(get_queued(), QUEUED_OUTPUT)
def test_private_queued_job(self):
"""Test: Correctly print queued private job"""
with patch("ubuntutools.running_autopkgtests._get_jobs", return_value=PRIVATE_JOB):
self.assertEqual(get_queued(), PRIVATE_OUTPUT)

View File

@ -17,9 +17,7 @@
"""Test suite for ubuntutools.update_maintainer""" """Test suite for ubuntutools.update_maintainer"""
import os import os
# import sys
import unittest import unittest
from io import StringIO from io import StringIO
from unittest import mock from unittest import mock
@ -167,8 +165,7 @@ Source: seahorse-plugins
Section: gnome Section: gnome
Priority: optional Priority: optional
Maintainer: Emilio Pozuelo Monfort <pochu@debian.org> Maintainer: Emilio Pozuelo Monfort <pochu@debian.org>
Build-Depends: debhelper (>= 5), Build-Depends: debhelper (>= 5)
cdbs (>= 0.4.41)
Standards-Version: 3.8.3 Standards-Version: 3.8.3
Homepage: http://live.gnome.org/Seahorse Homepage: http://live.gnome.org/Seahorse
@ -186,8 +183,7 @@ Section: gnome
Priority: optional Priority: optional
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com> Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
XSBC-Original-Maintainer: Emilio Pozuelo Monfort <pochu@debian.org> XSBC-Original-Maintainer: Emilio Pozuelo Monfort <pochu@debian.org>
Build-Depends: debhelper (>= 5), Build-Depends: debhelper (>= 5)
cdbs (>= 0.4.41)
Standards-Version: 3.8.3 Standards-Version: 3.8.3
Homepage: http://live.gnome.org/Seahorse Homepage: http://live.gnome.org/Seahorse
@ -200,25 +196,25 @@ class UpdateMaintainerTestCase(unittest.TestCase):
"""TestCase object for ubuntutools.update_maintainer""" """TestCase object for ubuntutools.update_maintainer"""
_directory = "/" _directory = "/"
_files = { _files = {"changelog": None, "control": None, "control.in": None, "rules": None}
"changelog": None,
"control": None,
"control.in": None,
"rules": None,
}
def _fake_isfile(self, filename): def _fake_isfile(self, filename):
"""Check only for existing fake files.""" """Check only for existing fake files."""
directory, base = os.path.split(filename) directory, base = os.path.split(filename)
return (directory == self._directory and base in self._files and return (
self._files[base] is not None) directory == self._directory and base in self._files and self._files[base] is not None
)
def _fake_open(self, filename, mode='r'): def _fake_open(self, filename, mode="r", encoding=None):
"""Provide StringIO objects instead of real files.""" """Provide StringIO objects instead of real files."""
self.assertTrue(encoding, f"encoding for {filename} not specified")
directory, base = os.path.split(filename) directory, base = os.path.split(filename)
if (directory != self._directory or base not in self._files or if (
(mode == "r" and self._files[base] is None)): directory != self._directory
raise IOError("No such file or directory: '%s'" % filename) or base not in self._files
or (mode == "r" and self._files[base] is None)
):
raise IOError(f"No such file or directory: '{filename}'")
if mode == "w": if mode == "w":
self._files[base] = StringIO() self._files[base] = StringIO()
self._files[base].close = lambda: None self._files[base].close = lambda: None
@ -228,11 +224,11 @@ class UpdateMaintainerTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
m = mock.mock_open() m = mock.mock_open()
m.side_effect = self._fake_open m.side_effect = self._fake_open
patcher = mock.patch('builtins.open', m) patcher = mock.patch("builtins.open", m)
self.addCleanup(patcher.stop) self.addCleanup(patcher.stop)
patcher.start() patcher.start()
m = mock.MagicMock(side_effect=self._fake_isfile) m = mock.MagicMock(side_effect=self._fake_isfile)
patcher = mock.patch('os.path.isfile', m) patcher = mock.patch("os.path.isfile", m)
self.addCleanup(patcher.stop) self.addCleanup(patcher.stop)
patcher.start() patcher.start()
self._files["rules"] = StringIO(_SIMPLE_RULES) self._files["rules"] = StringIO(_SIMPLE_RULES)
@ -260,8 +256,8 @@ class UpdateMaintainerTestCase(unittest.TestCase):
def test_original_ubuntu_maintainer(self): def test_original_ubuntu_maintainer(self):
"""Test: Original maintainer is Ubuntu developer. """Test: Original maintainer is Ubuntu developer.
The Maintainer field needs to be update even if The Maintainer field needs to be update even if
XSBC-Original-Maintainer has an @ubuntu.com address.""" XSBC-Original-Maintainer has an @ubuntu.com address."""
self._files["changelog"] = StringIO(_LUCID_CHANGELOG) self._files["changelog"] = StringIO(_LUCID_CHANGELOG)
self._files["control"] = StringIO(_AXIS2C_CONTROL) self._files["control"] = StringIO(_AXIS2C_CONTROL)
update_maintainer(self._directory) update_maintainer(self._directory)
@ -288,12 +284,11 @@ class UpdateMaintainerTestCase(unittest.TestCase):
def test_comments_in_control(self): def test_comments_in_control(self):
"""Test: Update Maintainer field in a control file containing """Test: Update Maintainer field in a control file containing
comments.""" comments."""
self._files["changelog"] = StringIO(_LUCID_CHANGELOG) self._files["changelog"] = StringIO(_LUCID_CHANGELOG)
self._files["control"] = StringIO(_SEAHORSE_PLUGINS_CONTROL) self._files["control"] = StringIO(_SEAHORSE_PLUGINS_CONTROL)
update_maintainer(self._directory) update_maintainer(self._directory)
self.assertEqual(self._files["control"].getvalue(), self.assertEqual(self._files["control"].getvalue(), _SEAHORSE_PLUGINS_UPDATED)
_SEAHORSE_PLUGINS_UPDATED)
def test_skip_smart_rules(self): def test_skip_smart_rules(self):
"""Test: Skip update when XSBC-Original in debian/rules.""" """Test: Skip update when XSBC-Original in debian/rules."""

View File

@ -16,12 +16,12 @@
"""This module is for updating the Maintainer field of an Ubuntu package.""" """This module is for updating the Maintainer field of an Ubuntu package."""
import logging
import os import os
import re import re
import debian.changelog import debian.changelog
import logging
Logger = logging.getLogger(__name__) Logger = logging.getLogger(__name__)
# Prior May 2009 these Maintainers were used: # Prior May 2009 these Maintainers were used:
@ -37,26 +37,26 @@ class MaintainerUpdateException(Exception):
pass pass
class Control(object): class Control:
"""Represents a debian/control file""" """Represents a debian/control file"""
def __init__(self, filename): def __init__(self, filename):
assert os.path.isfile(filename), "%s does not exist." % (filename) assert os.path.isfile(filename), f"{filename} does not exist."
self._filename = filename self._filename = filename
self._content = open(filename).read() self._content = open(filename, encoding="utf-8").read()
def get_maintainer(self): def get_maintainer(self):
"""Returns the value of the Maintainer field.""" """Returns the value of the Maintainer field."""
maintainer = re.search("^Maintainer: ?(.*)$", self._content, maintainer = re.search("^Maintainer: ?(.*)$", self._content, re.MULTILINE)
re.MULTILINE)
if maintainer: if maintainer:
maintainer = maintainer.group(1) maintainer = maintainer.group(1)
return maintainer return maintainer
def get_original_maintainer(self): def get_original_maintainer(self):
"""Returns the value of the XSBC-Original-Maintainer field.""" """Returns the value of the XSBC-Original-Maintainer field."""
orig_maintainer = re.search("^(?:[XSBC]*-)?Original-Maintainer: ?(.*)$", orig_maintainer = re.search(
self._content, re.MULTILINE) "^(?:[XSBC]*-)?Original-Maintainer: ?(.*)$", self._content, re.MULTILINE
)
if orig_maintainer: if orig_maintainer:
orig_maintainer = orig_maintainer.group(1) orig_maintainer = orig_maintainer.group(1)
return orig_maintainer return orig_maintainer
@ -65,38 +65,38 @@ class Control(object):
"""Saves the control file.""" """Saves the control file."""
if filename: if filename:
self._filename = filename self._filename = filename
control_file = open(self._filename, "w") control_file = open(self._filename, "w", encoding="utf-8")
control_file.write(self._content) control_file.write(self._content)
control_file.close() control_file.close()
def set_maintainer(self, maintainer): def set_maintainer(self, maintainer):
"""Sets the value of the Maintainer field.""" """Sets the value of the Maintainer field."""
pattern = re.compile("^Maintainer: ?.*$", re.MULTILINE) pattern = re.compile("^Maintainer: ?.*$", re.MULTILINE)
self._content = pattern.sub("Maintainer: " + maintainer, self._content) self._content = pattern.sub(f"Maintainer: {maintainer}", self._content)
def set_original_maintainer(self, original_maintainer): def set_original_maintainer(self, original_maintainer):
"""Sets the value of the XSBC-Original-Maintainer field.""" """Sets the value of the XSBC-Original-Maintainer field."""
original_maintainer = "XSBC-Original-Maintainer: " + original_maintainer original_maintainer = f"XSBC-Original-Maintainer: {original_maintainer}"
if self.get_original_maintainer(): if self.get_original_maintainer():
pattern = re.compile("^(?:[XSBC]*-)?Original-Maintainer:.*$", pattern = re.compile("^(?:[XSBC]*-)?Original-Maintainer:.*$", re.MULTILINE)
re.MULTILINE)
self._content = pattern.sub(original_maintainer, self._content) self._content = pattern.sub(original_maintainer, self._content)
else: else:
pattern = re.compile("^(Maintainer:.*)$", re.MULTILINE) pattern = re.compile("^(Maintainer:.*)$", re.MULTILINE)
self._content = pattern.sub(r"\1\n" + original_maintainer, self._content = pattern.sub(f"\\1\\n{original_maintainer}", self._content)
self._content)
def remove_original_maintainer(self): def remove_original_maintainer(self):
"""Strip out out the XSBC-Original-Maintainer line""" """Strip out out the XSBC-Original-Maintainer line"""
pattern = re.compile("^(?:[XSBC]*-)?Original-Maintainer:.*?$.*?^", pattern = re.compile(
re.MULTILINE | re.DOTALL) "^(?:[XSBC]*-)?Original-Maintainer:.*?$.*?^", re.MULTILINE | re.DOTALL
self._content = pattern.sub('', self._content) )
self._content = pattern.sub("", self._content)
def _get_distribution(changelog_file): def _get_distribution(changelog_file):
"""get distribution of latest changelog entry""" """get distribution of latest changelog entry"""
changelog = debian.changelog.Changelog(open(changelog_file), strict=False, changelog = debian.changelog.Changelog(
max_blocks=1) open(changelog_file, encoding="utf-8"), strict=False, max_blocks=1
)
distribution = changelog.distributions.split()[0] distribution = changelog.distributions.split()[0]
# Strip things like "-proposed-updates" or "-security" from distribution # Strip things like "-proposed-updates" or "-security" from distribution
return distribution.split("-", 1)[0] return distribution.split("-", 1)[0]
@ -107,25 +107,24 @@ def _find_files(debian_directory, verbose):
Returns (changelog, control files list) Returns (changelog, control files list)
Raises an exception if none can be found. Raises an exception if none can be found.
""" """
possible_contol_files = [os.path.join(debian_directory, f) for possible_contol_files = [os.path.join(debian_directory, f) for f in ["control.in", "control"]]
f in ["control.in", "control"]]
changelog_file = os.path.join(debian_directory, "changelog") changelog_file = os.path.join(debian_directory, "changelog")
control_files = [f for f in possible_contol_files if os.path.isfile(f)] control_files = [f for f in possible_contol_files if os.path.isfile(f)]
# Make sure that a changelog and control file is available # Make sure that a changelog and control file is available
if len(control_files) == 0: if len(control_files) == 0:
raise MaintainerUpdateException( raise MaintainerUpdateException(f"No control file found in {debian_directory}.")
"No control file found in %s." % debian_directory)
if not os.path.isfile(changelog_file): if not os.path.isfile(changelog_file):
raise MaintainerUpdateException( raise MaintainerUpdateException(f"No changelog file found in {debian_directory}.")
"No changelog file found in %s." % debian_directory)
# If the rules file accounts for XSBC-Original-Maintainer, we should not # If the rules file accounts for XSBC-Original-Maintainer, we should not
# touch it in this package (e.g. the python package). # touch it in this package (e.g. the python package).
rules_file = os.path.join(debian_directory, "rules") rules_file = os.path.join(debian_directory, "rules")
if os.path.isfile(rules_file) and \ if (
'XSBC-Original-' in open(rules_file).read(): os.path.isfile(rules_file)
and "XSBC-Original-" in open(rules_file, encoding="utf-8").read()
):
if verbose: if verbose:
print("XSBC-Original is managed by 'rules' file. Doing nothing.") print("XSBC-Original is managed by 'rules' file. Doing nothing.")
control_files = [] control_files = []
@ -161,8 +160,8 @@ def update_maintainer(debian_directory, verbose=False):
if original_maintainer.strip().lower() in _PREVIOUS_UBUNTU_MAINTAINER: if original_maintainer.strip().lower() in _PREVIOUS_UBUNTU_MAINTAINER:
if verbose: if verbose:
print("The old maintainer was: %s" % original_maintainer) print(f"The old maintainer was: {original_maintainer}")
print("Resetting as: %s" % _UBUNTU_MAINTAINER) print(f"Resetting as: {_UBUNTU_MAINTAINER}")
control.set_maintainer(_UBUNTU_MAINTAINER) control.set_maintainer(_UBUNTU_MAINTAINER)
control.save() control.save()
continue continue
@ -178,12 +177,13 @@ def update_maintainer(debian_directory, verbose=False):
return return
if control.get_original_maintainer() is not None: if control.get_original_maintainer() is not None:
Logger.warning("Overwriting original maintainer: %s", Logger.warning(
control.get_original_maintainer()) "Overwriting original maintainer: %s", control.get_original_maintainer()
)
if verbose: if verbose:
print("The original maintainer is: %s" % original_maintainer) print(f"The original maintainer is: {original_maintainer}")
print("Resetting as: %s" % _UBUNTU_MAINTAINER) print(f"Resetting as: {_UBUNTU_MAINTAINER}")
control.set_original_maintainer(original_maintainer) control.set_original_maintainer(original_maintainer)
control.set_maintainer(_UBUNTU_MAINTAINER) control.set_maintainer(_UBUNTU_MAINTAINER)
control.save() control.save()
@ -194,7 +194,7 @@ def update_maintainer(debian_directory, verbose=False):
def restore_maintainer(debian_directory, verbose=False): def restore_maintainer(debian_directory, verbose=False):
"""Restore the original maintainer""" """Restore the original maintainer"""
try: try:
changelog_file, control_files = _find_files(debian_directory, verbose) control_files = _find_files(debian_directory, verbose)[1]
except MaintainerUpdateException as e: except MaintainerUpdateException as e:
Logger.error(str(e)) Logger.error(str(e))
raise raise
@ -205,7 +205,7 @@ def restore_maintainer(debian_directory, verbose=False):
if not orig_maintainer: if not orig_maintainer:
continue continue
if verbose: if verbose:
print("Restoring original maintainer: %s" % orig_maintainer) print(f"Restoring original maintainer: {orig_maintainer}")
control.set_maintainer(orig_maintainer) control.set_maintainer(orig_maintainer)
control.remove_original_maintainer() control.remove_original_maintainer()
control.save() control.save()

79
ubuntutools/utils.py Normal file
View File

@ -0,0 +1,79 @@
# Copyright (C) 2019-2023 Canonical Ltd.
# Author: Brian Murray <brian.murray@canonical.com> et al.
# 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; version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Portions of archive related code that is re-used by various tools."""
import os
import re
import urllib.request
from datetime import datetime
import dateutil.parser
from dateutil.tz import tzutc
def get_cache_dir():
cache_dir = os.environ.get("XDG_CACHE_HOME", os.path.expanduser(os.path.join("~", ".cache")))
uat_cache = os.path.join(cache_dir, "ubuntu-archive-tools")
os.makedirs(uat_cache, exist_ok=True)
return uat_cache
def get_url(url, force_cached):
"""Return file to the URL, possibly caching it"""
cache_file = None
# ignore bileto urls wrt caching, they're usually too small to matter
# and we don't do proper cache expiry
m = re.search("ubuntu-archive-team.ubuntu.com/proposed-migration/([^/]*)/([^/]*)", url)
if m:
cache_dir = get_cache_dir()
cache_file = os.path.join(cache_dir, f"{m.group(1)}_{m.group(2)}")
else:
# test logs can be cached, too
m = re.search(
"https://autopkgtest.ubuntu.com/results/autopkgtest-[^/]*/([^/]*)/([^/]*)"
"/[a-z0-9]*/([^/]*)/([_a-f0-9]*)@/log.gz",
url,
)
if m:
cache_dir = get_cache_dir()
cache_file = os.path.join(
cache_dir, f"{m.group(1)}_{m.group(2)}_{m.group(3)}_{m.group(4)}.gz"
)
if cache_file:
try:
prev_mtime = os.stat(cache_file).st_mtime
except FileNotFoundError:
prev_mtime = 0
prev_timestamp = datetime.fromtimestamp(prev_mtime, tz=tzutc())
new_timestamp = datetime.now(tz=tzutc()).timestamp()
if force_cached:
return open(cache_file, "rb")
f = urllib.request.urlopen(url)
if cache_file:
remote_ts = dateutil.parser.parse(f.headers["last-modified"])
if remote_ts > prev_timestamp:
with open(f"{cache_file}.new", "wb") as new_cache:
for line in f:
new_cache.write(line)
os.rename(f"{cache_file}.new", cache_file)
os.utime(cache_file, times=(new_timestamp, new_timestamp))
f.close()
f = open(cache_file, "rb")
return f

View File

@ -17,28 +17,28 @@ import debian.debian_support
class Version(debian.debian_support.Version): class Version(debian.debian_support.Version):
def strip_epoch(self): def strip_epoch(self):
'''Removes the epoch from a Debian version string. """Removes the epoch from a Debian version string.
strip_epoch(1:1.52-1) will return "1.52-1" and strip_epoch(1.1.3-1) strip_epoch(1:1.52-1) will return "1.52-1" and strip_epoch(1.1.3-1)
will return "1.1.3-1". will return "1.1.3-1".
''' """
parts = self.full_version.split(':') parts = self.full_version.split(":")
if len(parts) > 1: if len(parts) > 1:
del parts[0] del parts[0]
version_without_epoch = ':'.join(parts) version_without_epoch = ":".join(parts)
return version_without_epoch return version_without_epoch
def get_related_debian_version(self): def get_related_debian_version(self):
'''Strip the ubuntu-specific bits off the version''' """Strip the ubuntu-specific bits off the version"""
related_debian_version = self.full_version related_debian_version = self.full_version
uidx = related_debian_version.find('ubuntu') uidx = related_debian_version.find("ubuntu")
if uidx > 0: if uidx > 0:
related_debian_version = related_debian_version[:uidx] related_debian_version = related_debian_version[:uidx]
uidx = related_debian_version.find('build') uidx = related_debian_version.find("build")
if uidx > 0: if uidx > 0:
related_debian_version = related_debian_version[:uidx] related_debian_version = related_debian_version[:uidx]
return Version(related_debian_version) return Version(related_debian_version)
def is_modified_in_ubuntu(self): def is_modified_in_ubuntu(self):
'''Did Ubuntu modify this (and mark the version appropriately)?''' """Did Ubuntu modify this (and mark the version appropriately)?"""
return 'ubuntu' in self.full_version return "ubuntu" in self.full_version

View File

@ -14,13 +14,18 @@
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
import optparse # pylint: disable=invalid-name
# pylint: enable=invalid-name
import argparse
import os import os
import sys import sys
from ubuntutools.update_maintainer import (update_maintainer, from ubuntutools.update_maintainer import (
restore_maintainer, MaintainerUpdateException,
MaintainerUpdateException) restore_maintainer,
update_maintainer,
)
def find_debian_dir(depth=6): def find_debian_dir(depth=6):
@ -30,42 +35,42 @@ def find_debian_dir(depth=6):
:rtype: str :rtype: str
:returns: a path to an existing debian/ directory, or None :returns: a path to an existing debian/ directory, or None
""" """
for path in ['../'*n or './' for n in list(range(0, depth+1))]: for path in ["../" * n or "./" for n in list(range(0, depth + 1))]:
debian_path = '{}debian'.format(path) debian_path = f"{path}debian"
if os.path.exists(os.path.join(debian_path, 'control')) \ if os.path.exists(os.path.join(debian_path, "control")) and os.path.exists(
and os.path.exists(os.path.join(debian_path, 'changelog')): os.path.join(debian_path, "changelog")
):
return debian_path return debian_path
return None return None
def main(): def main():
script_name = os.path.basename(sys.argv[0]) script_name = os.path.basename(sys.argv[0])
usage = "%s [options]" % (script_name) epilog = f"See {script_name}(1) for more info."
epilog = "See %s(1) for more info." % (script_name) parser = argparse.ArgumentParser(epilog=epilog)
parser = optparse.OptionParser(usage=usage, epilog=epilog) parser.add_argument(
parser.add_option("-d", "--debian-directory", dest="debian_directory", "-d",
help="location of the 'debian' directory (default: " "--debian-directory",
"%default).", metavar="PATH", dest="debian_directory",
default=find_debian_dir() or './debian') help="location of the 'debian' directory (default: %(default)s).",
parser.add_option("-r", "--restore", metavar="PATH",
help="Restore the original maintainer", default=find_debian_dir() or "./debian",
action='store_true', default=False) )
parser.add_option("-q", "--quiet", help="print no informational messages", parser.add_argument(
dest="quiet", action="store_true", default=False) "-r", "--restore", help="Restore the original maintainer", action="store_true"
(options, args) = parser.parse_args() )
parser.add_argument(
"-q", "--quiet", help="print no informational messages", dest="quiet", action="store_true"
)
args = parser.parse_args()
if len(args) != 0: if not args.restore:
print("%s: Error: Unsupported additional parameters specified: %s"
% (script_name, ", ".join(args)), file=sys.stderr)
sys.exit(1)
if not options.restore:
operation = update_maintainer operation = update_maintainer
else: else:
operation = restore_maintainer operation = restore_maintainer
try: try:
operation(options.debian_directory, not options.quiet) operation(args.debian_directory, not args.quiet)
except MaintainerUpdateException: except MaintainerUpdateException:
sys.exit(1) sys.exit(1)