Making a deb package with CMake/CPack and hosting it in a private APT repository
Last time I needed to handle a C++ library project with CMake. This time I was tasked with creating a deb package for one of the libraries in our SDK.
And what would you know, CMake can handle packaging too - with CPack utility.
About CPack
CPack, like CMake itself, is a CLI program. While CMake handles building the project, CPack is responsible for making packages.
The package is simply a “distribution unit” - something that you share with your users, or, in other words, what form your library/SDK/application is delivered in. Quite often it’s just a ZIP archive, but it can be also something more sophisticated, such as a package in one of the package management system formats, such as NuGet, RPM, deb and so on.
CPack comes especially handy if you need to support more than one package format. Instead of dealing with every single package format structure and requirements “manually”, you can let CPack handle all of them, so you’ll only need to worry about getting the actual package contents together.
In my case I needed to make a deb package for one of the libraries of our SDK, so deb package format will be the main focus of this article. Additional complexity of the task was that I needed to make a package only for that library and not for the entire SDK.
An example project
Let’s take the same project that was used as an example in the C++ library article, but make it slightly more complex: now it will have one more library (just to increase the number of components in the project, the main application doesn’t even link to it).
Here’s the project structure:
├── cmake
│ ├── InstallingConfigs.cmake
│ ├── InstallingGeneral.cmake
│ └── Packing.cmake
├── libraries
│ ├── AnotherLibrary
│ │ ├── include
│ │ │ └── another.h
│ │ ├── src
│ │ │ ├── another.cpp
│ │ │ └── things.h
│ │ ├── CMakeLists.txt
│ │ └── Config.cmake.in
│ ├── SomeLibrary
│ │ ├── include
│ │ │ └── some.h
│ │ ├── src
│ │ │ ├── some.cpp
│ │ │ └── things.h
│ │ ├── CMakeLists.txt
│ │ └── Config.cmake.in
│ └── CMakeLists.txt
├── CMakeLists.txt
├── LICENSE
├── README.md
└── main.cpp
For the reference, full project source code is available here.
What is needed to enable packing
Most of the work is done by install()
statements. So if you already have installation instructions in your project, then there is not much left for you to do.
As a bare minimum, you need to set some variables for CPack, such as package name and the way you’d like components to be (or not to be) grouped. Then you just include a standard module named CPack
into your project.
I put all that into a separate CMake module in the project (/cmake/Packing.cmake
):
# these are cache variables, so they could be overwritten with -D,
set(CPACK_PACKAGE_NAME ${PROJECT_NAME}
CACHE STRING "The resulting package name"
)
# which is useful in case of packing only selected components instead of the whole thing
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Simple C++ application"
CACHE STRING "Package description for the package metadata"
)
set(CPACK_PACKAGE_VENDOR "Some Company")
set(CPACK_VERBATIM_VARIABLES YES)
set(CPACK_PACKAGE_INSTALL_DIRECTORY ${CPACK_PACKAGE_NAME})
SET(CPACK_OUTPUT_FILE_PREFIX "${CMAKE_SOURCE_DIR}/_packages")
# https://unix.stackexchange.com/a/11552/254512
set(CPACK_PACKAGING_INSTALL_PREFIX "/opt/some")#/${CMAKE_PROJECT_VERSION}")
set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR})
set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH})
set(CPACK_PACKAGE_CONTACT "YOUR@E-MAIL.net")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "YOUR NAME")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE")
set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
# package name for deb. If set, then instead of some-application-0.9.2-Linux.deb
# you'll get some-application_0.9.2_amd64.deb (note the underscores too)
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
# that is if you want every group to have its own package,
# although the same will happen if this is not set (so it defaults to ONE_PER_GROUP)
# and CPACK_DEB_COMPONENT_INSTALL is set to YES
set(CPACK_COMPONENTS_GROUPING ALL_COMPONENTS_IN_ONE)#ONE_PER_GROUP)
# without this you won't be able to pack only specified component
set(CPACK_DEB_COMPONENT_INSTALL YES)
include(CPack)
According to this, the package name (CPACK_PACKAGE_NAME
) should have at least one dash/hyphen, so you might want/need to set it like this:
# ${namespace} could be your main project name, or company, or whatever
set(CPACK_PACKAGE_NAME "${namespace}-${PROJECT_NAME}"
CACHE STRING "The resulting package name"
)
And of course the same applies if you pass it as -DCPACK_PACKAGE_NAME
on configuration.
Once you have the Packing.cmake
module, include it in the very end of the main project (/CMakeLists.txt
):
# where to find our CMake modules
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include(Packing)
To create a package you need to configure the project, build it and then execute cpack
:
$ cd /path/to/cmake-cpack-example
$ mkdir build && cd $_
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ..
$ cmake --build .
$ cpack -G DEB
CPack: Create package using DEB
CPack: Install projects
CPack: - Install project: some-application []
CPack: - Install component: AnotherLibrary
CPack: - Install component: SomeLibrary
CPack: - Install component: some-application
CPack: Create package
-- CPACK_DEBIAN_PACKAGE_DEPENDS not set, the package will have no dependencies.
CPack: - package: /path/to/cmake-cpack-example/_packages/some-application_0.9.2_amd64.deb generated.
As you can guess, -G DEB
means that CPack will create a deb package. The full list of supported package formats (or rather CPack generators) is available here.
Note that even though packing depends on install()
statements, actual installation step (--target install
) wasn’t required here.
But building the target is required. If you try to call cpack
without building anything, it will fail:
$ cpack -G DEB
CPack: Create package using DEB
CPack: Install projects
CPack: - Install project: some-application []
CPack: - Install component: SomeLibrary
CMake Error at /path/to/cmake-cpack-example/build/libraries/SomeLibrary/cmake_install.cmake:41 (file):
file INSTALL cannot find
"/path/to/cmake-cpack-example/build/libraries/SomeLibrary/libSomeLibrary.a":
No such file or directory.
Call Stack (most recent call first):
/path/to/cmake-cpack-example/build/libraries/cmake_install.cmake:42 (include)
/path/to/cmake-cpack-example/build/cmake_install.cmake:42 (include)
CPack Error: Error when generating package: some-application
If everything required for the package was built, and packing went fine, then you’ll get a package in the project root:
_packages
└── some-application_0.9.2_amd64.deb
Let’s take a look at what’s inside:
$ cd ../_packages/
$ dpkg-deb -R ./some-application_0.9.2_amd64.deb ./package
$ tree ./package/
./package/
├── DEBIAN
│ ├── control
│ └── md5sums
└── opt
└── some
├── bin
│ └── some-application
├── cmake
│ ├── AnotherLibraryConfig.cmake
│ ├── AnotherLibraryConfigVersion.cmake
│ ├── AnotherLibraryTargets-release.cmake
│ ├── AnotherLibraryTargets.cmake
│ ├── SomeLibraryConfig.cmake
│ ├── SomeLibraryConfigVersion.cmake
│ ├── SomeLibraryTargets-release.cmake
│ └── SomeLibraryTargets.cmake
├── include
│ ├── AnotherLibrary
│ │ └── another.h
│ └── SomeLibrary
│ └── some.h
└── lib
├── libAnotherLibrary.a
└── libSomeLibrary.a
As you can see, it’s the same content as what we would get in the install folder after running --install
/--target install
, but prefixed with the specified installation path (/opt/some
) and with the package meta-information and checksums in DEBIAN
folder on top.
Components
Well, that was easy. But what wasn’t easy is to find out how to tell CPack to pack only selected components instead of the entire project.
For selective packing you need to define so-called components in the install()
statements. If an install()
statement doesn’t have a COMPONENT
set, then it will default to Unspecified
component. So by default the entire project is an Unspecified
component, and that is what will get packed.
Here’s how you can set COMPONENT
in an install()
statement:
install(
TARGETS ${PROJECT_NAME}
EXPORT "${PROJECT_NAME}Targets"
COMPONENT ${PROJECT_NAME} # must be here, not any line lower
# these get default values from GNUInstallDirs, no need to set them
#RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} # bin
#LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} # lib
#ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} # lib
# except for public headers, as we want them to be inside a library folder
PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} # include
)
# ...
install(
FILES
"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
DESTINATION cmake
COMPONENT ${PROJECT_NAME}
)
It is important that in case of install(TARGETS ...)
the COMPONENT
value should go right after EXPORT
, otherwise it kind of doesn’t apply or applies to something else?.. I don’t quite understand, what is going on here, but that’s how it worked for me.
To check what components will get packed, you can print the CPACK_COMPONENTS_ALL
variable:
message(STATUS "Components to pack: ${CPACK_COMPONENTS_ALL}")
So here I’ve set COMPONENT
to every single install()
statement in the project, and here’s what I get on configuring the project:
-- Components to pack: AnotherLibrary;SomeLibrary;some-application
Out of curiosity, let’s comment out COMPONENT
for the application’s install()
and run configure again:
-- Components to pack: AnotherLibrary;SomeLibrary;Unspecified
So if you get Unspecified
in the CPACK_COMPONENTS_ALL
variable, then most definitely you have at least one install()
statement somewhere in the project without COMPONENT
set.
The very same CPACK_COMPONENTS_ALL
variable is how you control which components need to be packed. If I want to pack only SomeLibrary
and some-application
components, then I need to pass -DCPACK_COMPONENTS_ALL="SomeLibrary;some-application"
on configure (you might need to update your CMake for multi-target --build
instruction to work):
$ cd /path/to/cmake-cpack-example/build
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCPACK_COMPONENTS_ALL="SomeLibrary;some-application" \
..
$ cmake --build . --target "SomeLibrary;some-application"
$ cpack -G DEB
CPack: Create package using DEB
CPack: Install projects
CPack: - Install project: some-application []
CPack: - Install component: SomeLibrary
CPack: - Install component: some-application
CPack: Create package
-- CPACK_DEBIAN_PACKAGE_DEPENDS not set, the package will have no dependencies.
CPack: - package: /path/to/cmake-cpack-example/_packages/some-application_0.9.2_amd64.deb generated.
$ dpkg-deb -R ../_packages/some-application_0.9.2_amd64.deb ../_packages/package
$ tree ../_packages/package/
../_packages/package/
├── DEBIAN
│ ├── control
│ └── md5sums
└── opt
└── some
├── bin
│ └── some-application
├── cmake
│ ├── SomeLibraryConfig.cmake
│ ├── SomeLibraryConfigVersion.cmake
│ ├── SomeLibraryTargets-release.cmake
│ └── SomeLibraryTargets.cmake
├── include
│ └── SomeLibrary
│ └── some.h
└── lib
└── libSomeLibrary.a
Here we go, AnotherLibrary
wasn’t packed this time.
Sadly, CPACK_COMPONENTS_ALL
(and other useful CPack variables) can be set only on project configuration, not on cpack
run.
Preinstall target
As at first I ran CMake with default generator, it was using Unix Makefiles
(which is the default one on some systems, such as Mac OS). And for a long time I couldn’t understand why, even though I have set specific components to pack and built required targets, I was getting a Run preinstall target
step, which was building the entire project before packing the components I asked.
Here’s how this can be reproduced:
$ cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release \
-DCPACK_COMPONENTS_ALL="SomeLibrary;some-application" \
..
$ cmake --build . --target "SomeLibrary;some-application"
$ cpack -G DEB
CPack: Create package using DEB
CPack: Install projects
CPack: - Run preinstall target for: some-application
CPack: - Install project: some-application []
CPack: - Install component: SomeLibrary
CPack: - Install component: some-application
CPack: Create package
-- CPACK_DEBIAN_PACKAGE_DEPENDS not set, the package will have no dependencies.
CPack: - package: /path/to/cmake-cpack-example/_packages/some-application_0.9.2_amd64.deb generated.
In fact, even if you won’t explicitly build required targets (or anything at all), cpack
will be still fine, because this “preinstall target” will anyway build the entire project.
It will pack only what was passed in CPACK_COMPONENTS_ALL
, no problem here, but it will always build the entire project. That’s absolutely not good news, because our SDK takes quite some time to build, and either way it’s just a waste of resources to build everything, when you only need a (small) part of the project.
I don’t know if such behavior is by design or is it a bug, but solution/workaround is simple: do not use Unix Makefiles
generator. Instead, use Ninja
, Visual Studio *
, Xcode
- none of these invoke that “preinstall target”. Alternatively, you can still use Unix Makefiles
for configuring and building the project, but pass -DCPACK_CMAKE_GENERATOR=Ninja
to cpack
.
Keeping Lintian happy
The deb package produced by CPack above is already fine and can be installed with dpkg
. However, there is a tool for running checks on packages - Lintian - and it will not be happy about that package quality:
$ lintian /path/to/cmake-cpack-example/_packages/some-application_0.9.2_amd64.deb
E: some-application: changelog-file-missing-in-native-package
E: some-application: dir-or-file-in-opt opt/some/
E: some-application: dir-or-file-in-opt opt/some/bin/
E: some-application: dir-or-file-in-opt ... use --no-tag-display-limit to see all (or pipe to a file/program)
E: some-application: extended-description-is-empty
E: some-application: maintainer-address-missing YOUR NAME
E: some-application: no-copyright-file
E: some-application: unstripped-binary-or-object opt/some/bin/some-application
W: some-application: missing-depends-line
W: some-application: non-standard-dir-perm opt/ 0775 != 0755
W: some-application: non-standard-dir-perm opt/some/ 0775 != 0755
W: some-application: non-standard-dir-perm ... use --no-tag-display-limit to see all (or pipe to a file/program)
Some things we can’t / don’t want to do anything about, as they are meant for proper system package maintainers (which we are not), such as:
changelog-file-missing-in-native-package
- we don’t really have a changelog in a form that is ready to be packed;dir-or-file-in-opt
- only natural, as this is intentional;extended-description-is-empty
- yeah, we don’t have a description for every single library;no-copyright-file
- indeed, there isn’t one.
But some warnings can be easily fixed with the right CPack variables in /cmake/Packing.cmake
.
maintainer-address-missing
The maintainer name should contain not just his name but also his e-mail address. And you need to set CPACK_DEBIAN_PACKAGE_MAINTAINER
exactly so:
set(CPACK_PACKAGE_CONTACT "YOUR@E-MAIL.net")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "YOUR NAME <${CPACK_PACKAGE_CONTACT}>")
unstripped-binary-or-object
The some-application
executable needs to be stripped from debug symbols. I am a bit confused about the fact that it still has them despite the Release build configuration, but okay, you can explicitly strip binaries with this CPack variable:
set(CPACK_STRIP_FILES YES)
missing-depends-line
A good package should list its dependencies. This can be turned on with the following variable:
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS YES)
As a result, in the case of my silly project, the following line will get added to /some-application_0.9.2_amd64.deb/DEBIAN/control
:
Depends: libc6 (>= 2.2.5), libstdc++6 (>= 5.2)
For that to work you need to have dpkg-shlibdeps
utility installed.
non-standard-dir-perm
The installation path directory should have 0755
permissions:
set(
CPACK_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS
OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE
)
Hosting deb packages for APT
This is a bonus part: how to set-up an APT repository with restricted access to host your deb packages. Mostly based on this beautiful blog post.
Repository
The deb packages repository is comprised by:
- a certain folders structure (with actual packages inside);
- several special text files, describing the repository contents, namely
Packages
andRelease
(and their compressed/signed variations).
That’s really what is there to it. The rest is just the way you make the repository available to users, what transport is used to fetch packages from it.
Let’s say we configured and packed only SomeLibrary
component:
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCPACK_COMPONENTS_ALL="SomeLibrary" \
-DCPACK_PACKAGE_NAME="some-library" \
-DCPACK_PACKAGE_DESCRIPTION_SUMMARY="Some library that prints a line of text" \
..
$ cmake --build . --target SomeLibrary
$ cpack -G DEB
CPack: Create package using DEB
CPack: Install projects
CPack: - Install project: some-application []
CPack: - Install component: SomeLibrary
CPack: Create package
-- CPACK_DEBIAN_PACKAGE_DEPENDS not set, the package will have no dependencies.
CPack: - package: /path/to/cmake-cpack-example/_packages/some-library_0.9.2_amd64.deb generated.
And now we want to put it into an APT repository, so people could fetch and install it on their machines with apt install
, like any other package.
We’ll be of course using a Linux server for hosting and serving the repository, so let’s start with creating the following structure on that server:
/path/to/somewhere/
└── repository
├── dists
│ └── stable
│ └── main
│ └── binary-amd64
└── pool
└── main
└── some-library_0.9.2_amd64.deb
So the deb packages go to pool/main
, and we uploaded our first some-library_0.9.2_amd64.deb
there.
Now we need to create a list of available packages - the Packages
file plus its compressed variant - it is generated by dpkg-scanpackages
utility:
$ cd /path/to/somewhere/repository
$ dpkg-scanpackages -m --arch amd64 pool/ > ./dists/stable/main/binary-amd64/Packages
$ cat ./dists/stable/main/binary-amd64/Packages | gzip -9 > ./dists/stable/main/binary-amd64/Packages.gz
The -m
option allows to have several version of the package (should be the default assumption, I think, otherwise how does one have several versions of the package in the repository?). Without it, when you’ll have the next version of your package (some-library_0.9.3_amd64.deb
) and will scan the that folder again, you’ll get a warning, and only one package will be written to Packages
file:
dpkg-scanpackages: warning: package some-library (filename ./pool/main/some-library_0.9.2_amd64.deb) is repeat; ignored that one and using data from ./pool/main/some-library_0.9.3_amd64.deb!
dpkg-scanpackages: info: Wrote 1 entries to output Packages file.
And yes, the Packages
file and its compressed variant need to be updated every time you upload a new package.
Next thing we need to do is to prepare a Release
file. Just like Packages
, it also needs to be updated on every new uploaded package. And since its generation is a bit more complex, it’s a good idea to make a script for this task:
#!/bin/sh
set -e
do_hash() {
HASH_NAME=$1
HASH_CMD=$2
echo "${HASH_NAME}:"
for f in $(find -type f); do
f=$(echo $f | cut -c3-) # remove ./ prefix
if [ "$f" = "Release" ]; then
continue
fi
echo " $(${HASH_CMD} ${f} | cut -d" " -f1) $(wc -c $f)"
done
}
cat << EOF
Origin: Some repository
Label: Some
Suite: stable
Codename: stable
Version: 1.0
Architectures: amd64
Components: main
Description: Some software repository
Date: $(date -Ru)
EOF
do_hash "MD5Sum" "md5sum"
do_hash "SHA1" "sha1sum"
do_hash "SHA256" "sha256sum"
What the script does is it describes the repository and also provides checksums for the Packages
files. Save it as generate-release.sh
and execute to make the Release
file:
$ cd /path/to/somewhere/repository/dists/stable/
$ /path/to/elsewhere/generate-release.sh > ./Release
We should also sign the Release
file with PGP, and that will be covered a bit later, but the repository can already serve packages to APT as it is - you only need to set-up a transport for it.
Transport
APT supports several transports. SSH and HTTP/HTTPS I was aware of and will demonstrate them below, but apparently it can also use AWS S3 and Tor, which was news to me.
SSH
At first I thought that I can just expose the repository via SFTP using a chrooted user, and so that’s what I did on the repository server. My reasoning for going with SFTP was to use SSH keys (instead of Basic authentication in case of HTTP/HTTPS).
Start with adding a “guest” user:
$ sudo addgroup packages
$ sudo adduser --ingroup packages --home /home/packages packages
and move the repository to /home/packages/
:
$ mv /path/to/somewhere/repository /home/packages/
$ sudo chown -R packages:packages /home/packages
Allow chrooted SFTP access for that user in /etc/ssh/sshd_config
:
...
Subsystem sftp internal-sftp
...
Match Group packages
# force the connection to use SFTP and chroot to the required directory
ForceCommand internal-sftp
ChrootDirectory /home/packages
# disable tunneling
PermitTunnel no
# disable authentication agent
AllowAgentForwarding no
# disable TCP forwarding
AllowTcpForwarding no
# disable X11 forwarding
X11Forwarding no
# disable password, only key is allowed
PasswordAuthentication no
and restart SSH service:
$ sudo systemctl restart sshd.service
Generate SSH key, add its public part to /home/packages/.ssh/authorized_keys
on server. Create an entry in ~/.ssh/config
on client/user machine with private key (might need to do that for root user too, because apt
is likely to be called with sudo
).
Add a new APT source on the client/user machine in /etc/apt/sources.list.d/some.list
:
deb [arch=amd64 trusted=yes] ssh://your.host/repository stable main
The trusted=yes
parameter is required, since our repository is not signed yet.
Now try to fetch the packages information:
$ sudo apt update
That might fail:
The method 'ssh' is unsupported and disabled by default. Consider switching to http(s). Set Dir::Bin::Methods::ssh to "ssh" to enable it again
If you got that error, create /etc/apt/apt.conf.d/99ssh
file with the following content:
Dir::Bin::Methods::ssh "ssh";
and try again:
$ sudo apt update
...
Reading package lists... Done
E: Method ssh has died unexpectedly!
E: Sub-process ssh received signal 13.
This error happens because APT cannot work via SFTP only, as it actually needs proper SSH to run certain commands on the repository host. I find it pretty amusing, considering that APT works via HTTP/HTTPS just fine without the necessity to run remote commands.
But okay, our options then are to either make a “jailed chroot” (a very limited and restricted system environment) for the packages
user or to give it proper SSH access. I didn’t like either of these options, especially the last one, but just to finish this part, let’s consider the latter, as it’s the easiest.
Go to /etc/ssh/sshd_config
again and comment out these lines:
#ForceCommand internal-sftp
#ChrootDirectory /home/packages
Restart SSH service and now you should be able to fetch and install the packages:
$ sudo apt update
$ sudo apt install some-library
$ ls -l /opt/some
HTTPS
That seems to be a more common way of connecting to APT repositories. Too bad SSH option turned out to be not what I expected.
Of course, for HTTP/HTTPS you’ll need a web-server, and I’ll be using NGINX, but any other will be fine too (as long as it supports Basic authentication).
Move the repository to website root:
$ mv /path/to/somewhere/repository /var/www/your.host/files/packages/deb
$ sudo chown -R www-data:www-data /var/www/your.host/files/packages
Create a password for Basic authentication:
$ sudo htpasswd -c /etc/nginx/packages.htpasswd packages
Edit NGINX site config (/etc/nginx/sites-enabled/your.host
):
location /files/packages/deb/ {
root /var/www/your.host;
auth_basic "restricted area";
auth_basic_user_file /etc/nginx/packages.htpasswd;
#autoindex on;
try_files $uri $uri/ =404;
}
Add the new source on client/user machine to /etc/apt/sources.list.d/some.list
:
deb [arch=amd64 trusted=yes] https://your.host/files/packages/deb stable main
The trusted=yes
parameter is required, since our repository is not signed yet.
Also add credentials (Basic authentication) for that host into /etc/apt/auth.conf.d/some
:
machine your.host
login packages
password HERE-GOES-PASSWORD
Now you should be able to fetch and install packages:
$ sudo apt update
$ sudo apt install some-library
$ ls -l /opt/some
Secure APT
It is recommended not to use trusted=yes
parameter in APT source setting and instead to sign the repository with PGP key.
For that you’ll need to create a PGP key, if you don’t already have one:
$ gpg --full-generate-key
If you are doing this on the server via SSH, you’ll wait forever for the key generation to finish, because it expects some input from background activities to gain entropy. Connect to the same server from another tab and run something like:
$ dd if=/dev/sda of=/dev/zero
or maybe:
$ find / | xargs file
Alternatively, you can just generate the key on your local machine and import it on the server.
Once you have the key, export its public part:
$ gpg --list-secret-keys --keyid-format=long
/root/.gnupg/pubring.kbx
------------------------
sec rsa4096/KEY-ID 2021-09-18 [SCE]
SOME-OTHER-ID
uid [ultimate] Some Software <admin@your.host>
$ gpg --armor --export KEY-ID > /path/to/somewhere/repository/some-public.asc
$ cat /path/to/somewhere/repository/some-public.asc | gpg --list-packets
Just in case, check that there is only one :public key packet:
in the output, otherwise APT might get confused by such a key.
You can now sign the Release
file:
$ cd /path/to/somewhere/repository/dists/stable
$ export GPG_TTY=$(tty)
$ cat ./Release | gpg --default-key KEY-ID -abs > ./Release.gpg
$ cat ./Release | gpg --default-key KEY-ID -abs --clearsign > ./InRelease
If you protected the key with a password (you should have), then you’ll get the key password prompt. And if you’ll need to execute these commands “unattended” from a buildbot user in some CI/CD, then you’ll need to add --pinentry-mode=loopback
and --passphrase
options:
$ cat ./Release | gpg --default-key KEY-ID --pinentry-mode=loopback --passphrase "PGP-KEY-PASSWORD" -abs > ./Release.gpg
$ cat ./Release | gpg --default-key KEY-ID --pinentry-mode=loopback --passphrase "PGP-KEY-PASSWORD" -abs --clear-sign > ./InRelease
Since we are talking about buildbots, you’ll also need to make sure that they have access to the keychain. You might need to either move .gnupg
folder to buildbot’s home folder or set the GNUPGHOME
environment variable. In both cases don’t forget to change the ownership of that folder or set required permissions.
Now you can remove trusted=yes
from /etc/apt/sources.list.d/some.list
:
deb [arch=amd64] https://your.host/files/packages/deb stable main
If then you try to run apt update
, you’ll get an error about unknown key:
W: GPG error: https://your.host/files/packages/deb stable InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY KEY-ID
W: Failed to fetch https://your.host/files/packages/deb/dists/stable/InRelease The following signatures couldn't be verified because the public key is not available: NO_PUBKEY KEY-ID
Put the public key (some-public.asc
) somewhere public on the server and import it on client:
$ wget -O - https://your.host/some-public.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/some-keyring.gpg >/dev/null
Add signed-by
parameter to /etc/apt/sources.list.d/some.list
:
deb [arch=amd64 signed-by=/usr/share/keyrings/some-keyring.gpg] https://your.host/files/packages/deb stable main
Finally, it’s all good:
$ sudo apt update
$ sudo apt install some-library
$ tree /opt/some
/opt/some
├── cmake
│ ├── SomeLibraryConfig.cmake
│ ├── SomeLibraryConfigVersion.cmake
│ ├── SomeLibraryTargets-release.cmake
│ └── SomeLibraryTargets.cmake
├── include
│ └── SomeLibrary
│ └── some.h
└── lib
└── libSomeLibrary.a
Social networks
Zuck: Just ask
Zuck: I have over 4,000 emails, pictures, addresses, SNS
smb: What? How'd you manage that one?
Zuck: People just submitted it.
Zuck: I don't know why.
Zuck: They "trust me"
Zuck: Dumb fucks