Uploading to an Ubuntu PPA

Developing for Ubuntu is easy! Except when it's not.

The Ubuntu packaging doc and Gert van Dijk's tutorial are better than this, but I found them hard to follow the first few times, so I documented one run through the process, warts and all.

Create the PPA on launchpad

Visit your Launchpad home page and click "Create a new PPA". In my case, I'm going to be uploading fixes for python packages, so I'm calling mine python-fixes.

Create a fresh environment and a GPG key

You need a GPG key to upload to a PPA, and a clean place to do the builds. The Ubuntu packaging doc explains how to generate a GPG key, but there are many possible gotchas along the way. So here's a log of setting both up, showing the workarounds I had to use. (I'm using lxc rather than pbuilder, so your mileage may vary.) Replace dank@kegel.com with your email address, etc.
$ lxc launch ubuntu:18.04 ppa-ubu1804
$ lxc exec ppa-ubu1804 bash
# apt update
# apt dist-upgrade
# su ubuntu
$ gpg --gen-key
...
gpg: agent_genkey failed: Inappropriate ioctl for device
: D'oh!  gpg is confused by the virtual environment.  Give it a hint.
$ export GPG_TTY=$(tty)
$ gpg --gen-key
..
gpg: agent_genkey failed: No such file or directory
: D'oh!  gpg is confused by lack of an agent.  Set one up.
$ cat > .gnupg/gpg.conf <<_EOF_
use-agent
pinentry-mode loopback
_EOF_
$ cat > ~/.gnupg/gpg-agent.conf <<_EOF_
allow-loopback-pinentry
_EOF_
$ echo RELOADAGENT | gpg-connect-agent
OK
$ gpg --gen-key
...
Real name: Dan Kegel
Email address: dank@kegel.com
...
Enter passphrase:
...
public and secret key created and signed
pub   rsa3072 2019-03-23 [SC] [expires: 2021-03-22]
      841D18F50B873C06B7690B6F50A93FF65680EA47
...
$ gpg -a --export-secret-keys > myprivatekeys.asc
$ gpg --export-ownertrust > otrust.txt
$ gpg --send-keys --keyserver keyserver.ubuntu.com 841D18F50B873C06B7690B6F50A93FF65680EA47
: Not sure if following line works right away, but it's way faster then
: visiting the web site to check.
$ gpg --keyserver hkp://keyserver.ubuntu.com --search-key dank@kegel.com
gpg: data source: http://91.189.89.49:11371
(1)	Dan Kegel <dank@kegel.com>
	  3072 bit RSA key 50A93FF65680EA47, created: 2019-03-23, expires:
2021-03-22
...
Once gpg --search-key can find your key on the keyserver, you're ready to move on to the next step. But first save myprivatekeys.asc and otrust.txt somewhere safe and permanent (like google drive?), and save the passphrase in your master password file! In a month you'll need that passphrase again, and if you're like me, you'll have forgotten it utterly. e.g. on the host system:
$ mkdir my-gpg-key-backup
$ lxc file pull ppa-ubu1804/home/ubuntu/myprivatekeys.asc my-gpg-key-backup
$ lxc file pull ppa-ubu1804/home/ubuntu/otrust.txt my-gpg-key-backup

Next, register your GPG key with your launchpad.net account as described in the Ubuntu packaging doc. It will send you an email with an encrypted block; copy that block with your mouse and paste it into a file, then decrypt it (on a system with the GPG key you created) using gpg --decrypt. That will tell you how to complete the registration.

Finally, copy the ~/.ssh/id_rsa you registered with Launchpad to the ubuntu user's ~/.ssh directory, e.g.

$ lxc file push .ssh/id_rsa ppa-ubu1804/home/ubuntu/.ssh/

Set up your environment for building Ubuntu packages

Follow the tips in the Ubuntu packaging doc and/or Gert van Dijk's tutorial, e.g.
$ sudo apt-get install ubuntu-dev-tools apt-file
$ echo DSCVERIFY_KEYRINGS="/etc/apt/trusted.gpg:/usr/share/keyrings/debian-maintainers.gpg:~/.gnupg/pubring.gpg" > ~/.devscripts
$ echo 'export DEBEMAIL="dank@kegel.com"' >> ~/.profile
$ echo 'export DEBFULLNAME="Dan Kegel"' >> ~/.profile
$ . ~/.profile
$ cat > ~/.dput.cf <<_EOF_
[python-fixes]
fqdn = ppa.launchpad.net
incoming = ~dank/ubuntu/python-fixes/
method = sftp
login = dank
ssh_config_options = IdentityFile ~/.ssh/id_rsa_launchpad
_EOF_

Finally, build the package of interest

I'm just cherrypicking one patch, so:
$ sudo sed -i 's/^# deb-src/deb-src/' /etc/apt/sources.list
$ sudo apt update
$ sudo apt build-dep python-pip
$ apt source python-pip
$ wget https://github.com/pypa/pip/commit/018f03aab448231462b6c013a65d60a35e06e65a.patch
$ cd python-pip-9.0.1
$ cd pip
: Note: first hunk won't apply, that's ok
$ patch -p4 < ~/018*patch
$ cd ..
: dch: Edit debian/changelog
$ dch -i
: dpkg-source: adds the code change as a patch in debian/patches
: Be sure to trim unrelated crud from the patch
$ dpkg-source --commit
dpkg-source: info: local changes detected, the modified files are:
 python-pip-9.0.1/pip/wheel.py
Enter the desired patch name: pypa-pip-issue-5366-crash.patch
: Back up the source directory, as debuild is sometimes destructive
$ cp -a . ../python-pip-9.0.1.bak
$ debuild
Verify that the built package works, then do a source-only build, and upload the changes file to your PPA. e.g.
$ cd ..
$ sudo dpkg -i python-pip*.deb
$ ...
$ mv python-pip-9.0.1 python-pip-9.0.1.dirty
$ cp -a python-pip-9.0.1.bak python-pip-9.0.1
$ cd python-pip-9.0.1
$ debuild -S
$ dput ppa:dank/python-fixes python-pip_9.0.1-2.3~ubuntu2_amd64.changes

Watch for the PPA build results

You should get an email with the results of your upload within about a minute telling you what you did wrong :-)

After a few uploads, if you're lucky, the upload will be accepted, and the build should show up in your ppa page (e.g. https://launchpad.net/~dank/+archive/ubuntu/python-fixes).

Do it again for the next platform

This time, don't generate a gpg key, use the one from the previous platform.

Starting from the outer host again, here's the one-time setup:

$ lxc launch ubuntu:16.04 ppa-ubu1604
$ lxc file push my-gpg-key-backup/myprivatekeys.asc ppa-ubu1604/home/ubuntu/myprivatekeys.asc
$ lxc file push my-gpg-key-backup/otrust.txt ppa-ubu1604/home/ubuntu/otrust.txt
$ lxc file push .ssh/id_rsa ppa-ubu1604/home/ubuntu/.ssh/id_rsa
$ lxc exec ppa-ubu1604 bash
# apt update
# apt dist-upgrade
# su ubuntu
$ gpg --import myprivatekeys.asc 
$ gpg --import-ownertrust otrust.txt 
$ sudo apt-get install ubuntu-dev-tools apt-file
$ echo DSCVERIFY_KEYRINGS="/etc/apt/trusted.gpg:/usr/share/keyrings/debian-maintainers.gpg:~/.gnupg/pubring.gpg" > ~/.devscripts
$ echo 'export DEBEMAIL="dank@kegel.com"' >> ~/.profile
$ echo 'export DEBFULLNAME="Dan Kegel"' >> ~/.profile
$ . ~/.profile
$ cat > ~/.dput.cf <<_EOF_
[python-fixes]
fqdn = ppa.launchpad.net
incoming = ~dank/ubuntu/python-fixes/
login = dank
ssh_config_options = IdentityFile ~/.ssh/id_rsa_launchpad
_EOF_
$ sudo sed -i 's/^# deb-src/deb-src/' /etc/apt/sources.list
$ sudo apt update
And here's fixing the package of interest:
$ sudo apt build-dep python-pip
$ apt source python-pip
$ wget https://github.com/pypa/pip/commit/018f03aab448231462b6c013a65d60a35e06e65a.patch
$ cd python-pip-8.1.1
$ cd pip
$ patch -p4 < ~/018*patch
$ cd ..
: dch: Edit debian/changelog
$ dch -i
: dpkg-source: adds the code change as a patch in debian/patches
: Be sure to trim unrelated crud from the patch
$ dpkg-source --commit
$ cp -a . ../python-pip-8.1.1.bak
$ debuild
$ cd ..
$ ... test it ...
$ mv python-pip-8.1.1 python-pip-8.1.1.dirty
$ cp -a python-pip-8.1.1.bak python-pip-8.1.1
$ cd python-pip-8.1.1
$ debuild -S
$ cd ..
$ dput ppa:dank/python-fixes python-pip_8.1.1-2ubuntu0.5_source.changes

Profit!