This document describes how packages are built for Bottlerocket.
In the Background section, we discuss the general approach, the specific technologies in use, and the rationale behind it all.
In the Development section, we provide a short guide for adding a new package.
Like any Linux distribution, Bottlerocket builds on a foundation of open source software, from the Linux kernel through the GNU C Library and more. Unlike most distributions, it is not designed to be self-hosting. We want to package and maintain only the software that we eventually ship, and not the software needed to build that software.
Bottlerocket makes extensive use of cross compilation as a result.
Cross compilation involves at least two conceptually distinct systems: the host and the target.
The host is another Linux distribution that provides the toolchain and other tools needed to build a modern Linux userspace.
This includes perl
, python
, make
, and meson
.
The target is Bottlerocket.
It includes only the packages found in this directory.
Because Bottlerocket uses image-based updates, it does not need a package manager - yet the packages are defined in RPM spec files and built using RPM. Why?
The separation of responsibilities between host and target outlined above is not quite enough to achieve the goal of a minimal footprint.
Many of the packages we build contain both the shared libraries we need at runtime as well as the headers we need to build other software for our target.
RPM offers a familiar mechanism for separating the artifacts from a build into different packages.
For example, libseccomp is split into a libseccomp
package for runtime use, and a libseccomp-devel
package for building other packages.
With RPM it is idiomatic to define macros to standardize the invocation of scripts like configure
and tools like make
.
Since we are cross-compiling, many more environment variables must be set and arguments passed to ensure that builds use the target's toolchain and dependencies rather than the host's.
The spec files make extensive use of project-specific macros for this reason.
Macros also provide a way to ensure policy objectives are applied across all packages. Examples include stripping debug symbols, collecting software license information, and running security checks.
A key aspect of building RPMs - or any software - is providing a consistent and clean build environment. Otherwise, a prior build on the same host can change the result in surprising ways. mock is often used for this, either directly or by services such as Koji.
Bottlerocket uses Docker and containers to accomplish this instead. Every package build starts from a container with the Bottlerocket SDK and zero or more other packages installed as dependencies. Any source archives and patches needed for the build are copied in, and the binary RPMs are copied out once the build is complete.
Say we want to package libwoof
, the C library that provides the reference implementation for the WOOF framework.
This listing shows the directory structure of our sample package.
packages/libwoof/
├── Cargo.toml
├── libwoof.spec
Each package has a Cargo.toml
file that lists its build dependencies, runtime dependencies, and metadata such as external files and the expected hashes.
It also refers to a build.rs
build script which tells Cargo to invoke our buildsys tool.
The RPM packages we want are built as a side effect of Cargo running the script.
It points to /dev/null
for the actual crate, since Cargo expects some Rust code to build, and is happy with an empty file.
Finally, it includes a spec
file that defines the RPM.
Our sample package has the following manifest.
[package]
name = "libwoof"
version = "0.1.0"
edition = "2021"
publish = false
build = "../build.rs"
[lib]
path = "../packages.rs"
[[package.metadata.build-package.external-files]]
url = "http://downloads.sourceforge.net/libwoof/libwoof-1.0.0.tar.xz"
sha512 = "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
# RPM BuildRequires
[build-dependencies]
glibc = { path = "../glibc" }
libseccomp = { path = "../libseccomp" }
# RPM Requires
[dependencies]
# None
Be sure to include publish = false
for all packages, as these are not standard crates and should never appear on crates.io.
The package.metadata table is ignored by Cargo and interpreted by our buildsys
tool.
It contains an external-files
list which provides upstream URLs and expected hashes.
These files are, by default, only fetched from our upstream source mirror, using the URL template https://cache.bottlerocket.aws/{file}/{sha512}/{file}
.
(If file
is not defined, the text after the last /
character in url
is used.)
If your source is not yet available in the upstream source mirror, you can run cargo make
with -e BUILDSYS_UPSTREAM_SOURCE_FALLBACK=true
.
We use the dependencies and build-dependencies sections of Cargo.toml
to ensure additional packages are built.
Some packages depend on building other packages first because they're used directly by the build process.
These are expressed in RPM spec with BuildRequires:
.
We use the build-dependencies
section to ensure BuildRequires:
packages are built before buildsys is invoked for the current package.
We can omit a package from build-dependencies
if the Bottlerocket SDK provides it.
Some packages depend on other packages being available when they're installed because they're used dynamically at runtime.
These are expressed in RPM spec with Requires:
.
We use the dependencies
section to ensure packages needed at runtime are built.
We could specify these in build-dependencies
, but we prefer to separate them to indicate that they are not needed for the current package build.
We express Requires:
packages in the dependencies
section with the following exceptions:
- We omit a
Requires:
package if it is provided by the Bottlerocket SDK. - We omit a
Requires:
package if it is defined within the same RPM spec file thatRequires:
it. - We may omit a
Requires:
package if we know it will be built by some other requirement and we want to optimize certain developer workflows.
In this case, libwoof
depends on glibc
and libseccomp
at build-time.
We want those libraries to be built first, and for this one to be rebuilt when they are modified so we add these to the build-dependencies
.
libwoof
does not declare any runtime dependencies, so the dependencies
section is empty.
We reuse the same build script for all packages.
use std::process::{exit, Command};
fn main() -> Result<(), std::io::Error> {
let ret = Command::new("buildsys").arg("build-package").status()?;
if !ret.success() {
exit(1);
}
Ok(())
}
If you need a build script with different behavior, the recommended approach is to modify the buildsys
tool.
The package.metadata
table can be extended with declarative elements that enable the new feature.
Spec files will vary widely across packages, and a full guide to RPM packaging is out of scope here.
Name: %{_cross_os}libwoof
Version: 1.0.0
Release: 1%{?dist}
Summary: Library for woof
License: Apache-2.0 OR MIT
URL: http://sourceforge.net/projects/libwoof/
Source0: http://downloads.sourceforge.net/libwoof/libwoof-1.0.0.tar.xz
BuildRequires: %{_cross_os}glibc-devel
BuildRequires: %{_cross_os}libseccomp-devel
%description
%{summary}.
%package devel
Summary: Files for development using the library for woof
Requires: %{name}
%description devel
%{summary}.
%prep
%autosetup -n libwoof-%{version} -p1
%build
%cross_configure
%make_build
%install
%make_install
%files
%license LICENSE
%{_cross_libdir}/*.so.*
%files devel
%{_cross_libdir}/*.so
%dir %{_cross_includedir}/libwoof
%{_cross_includedir}/libwoof/*.h
%{_cross_pkgconfigdir}/*.pc
%changelog
Macros start with %
.
If the macro is specific to Bottlerocket, it will include the cross
token.
The definitions for most of these can be found in macros.
The definition for %{_cross_variant}
is the Bottlerocket variant being built.
When developing a package on an RPM-based system, you can expand the macros with a command like this.
$ PKG=libwoof
$ rpmspec \
--macros "/usr/lib/rpm/macros:macros/$(uname -m):macros/shared:macros/rust:macros/cargo" \
--define "_sourcedir packages/${PKG}" \
--parse packages/${PKG}/${PKG}.spec
The variants workspace's Cargo.lock
may be affected by adding a package.
cd
into thevariants
directory at the root of the repository.- Include the new package inside the
Cargo.toml
file of the variant you want to modify. - Run
cargo generate-lockfile
to refreshCargo.lock
.
To build your package, run the following command in the top-level Bottlerocket directory.
cargo make -e PACKAGE=libwoof build-package
This will build your package and its dependencies.