Containerfile for base forge actions

Build autoconf and automake and add autoregen.py from
https://sourceware.org/git/builder.git

Add forge action to build container images.

ChangeLog:

	* .forgejo/workflows/build-containers.yaml: New file.

contrib/ChangeLog:

	* ci-containers/README: New file.
	* ci-containers/autoregen/Containerfile: New file.
	* ci-containers/autoregen/autoregen.py: New file.
	* ci-containers/build-image.sh: New file.

Signed-off-by: Pietro Monteiro <pietro@sociotechnical.xyz>
This commit is contained in:
Pietro Monteiro
2026-01-08 07:31:40 -05:00
parent 7a8c00dea5
commit 17cc07b4d3
5 changed files with 435 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
on:
# push:
# branches:
# - trunk
# # run on changes to any file for ci containers or to this file
# paths:
# - .forgejo/workflows/build-containers.yaml
# - 'contrib/ci-containers/**/*'
# similar for pull requests
pull_request:
types: [opened, synchronize, reopened]
paths:
- .forgejo/workflows/build-containers.yaml
- 'contrib/ci-containers/**/*'
jobs:
containers:
runs-on: sourceware-runner
container:
image: fedora:latest
env:
# the default overlayfs doesn't work when running on docker, which uses overlayfs
STORAGE_DRIVER: vfs
# we can't run containers in docker, so use a chroot to build the image
BUILDAH_ISOLATION: chroot
steps:
- name: install dependencies
run: |
dnf -y --setopt=install_weak_deps=False install buildah git nodejs
# Checkout sources
- uses: actions/checkout@v4
- name: build containers
run: |
echo "Building containers from contrib/ci-containers"
for DIR in ./contrib/ci-containers/*
do
! [ -d "$DIR" ] && continue
CONTAINER="$(basename "$DIR")"
if [ "$FORGEJO_EVENT_NAME" = pull_request ]; then
# branch name in lowercase, replace non-alphanumerics with '-', and remove leading and trailling '-'
TAG="$(echo "$FORGEJO_HEAD_REF" | sed -e 's/\(.*\)/\L\1/' -e 's/[^[:alnum:]-]/-/g' -e 's/^-\+//;s/-\+$//')"
else
# branch name
TAG="$FORGEJO_REF_NAME"
fi
echo "Building $CONTAINER with tag $TAG"
./contrib/ci-containers/build-image.sh -d "$DIR" -t "$TAG" -- --network=host
echo "Built $CONTAINER:$TAG should push it somewhere"
buildah images --json "$CONTAINER:$TAG"
echo "Removing container image from localhost"
buildah rmi "$CONTAINER:$TAG"
buildah rmi --prune
done

View File

@@ -0,0 +1,47 @@
# CI Containers
Each subdirectory under `contrib/ci-containers/` holds a hermetic description of
a container image that powers jobs on the [Sourceware
Forge](https://forge.sourceware.org). The directory itself is used as the build
context, so any assets referenced by the `Containerfile` must be present
in the subdirectory.
Keeping the description self-contained guarantees reproducible builds.
## Building Images
Images are built with [buildah](https://buildah.io) via the helper script
`build-image.sh`. A typical invocation looks like:
```bash
./contrib/ci-containers/build-image.sh \
-d ./contrib/ci-containers/foo \
-t v1.0 \
-- --layers --no-cache
```
* `-d` - Path to the directory containing the `Containerfile`.
* `-t` - Tag to apply to the resulting image.
* The trailing `--` passes additional flags directly to `buildah` (here we
request layered output and disable the cache).
The full image tag will be the basename of the directory, in this case `foo`,
and the value passed to the `-t/--tag` argument. Our hypothetical image will be
tagged locally as `foo:v1.0`.
### Verify the build
```bash
buildah images --json foo:v1.0
```
The command returns a JSON object with the image's ID, size, and other metadata.
### Test the image locally
```bash
podman run --rm -it foo:v1.0 /bin/bash
```
By running the image interactively you can confirm that the environment behaves
as expected.

View File

@@ -0,0 +1,83 @@
FROM debian:stable-slim
ARG AUTOCONF_VERSION=2.69
ARG AUTOMAKE_VERSION=1.15.1
# Run time deps
RUN set -eux; \
apt-get update; \
apt-get upgrade -y; \
apt-get install -y --no-install-recommends \
autogen \
ca-certificates \
git \
m4 \
nodejs \
perl \
python3 \
python3-git \
python3-termcolor \
python3-unidiff \
wget; \
rm -rf /var/lib/apt/lists/*
# Get and install the autoregen.py script
COPY --chmod=755 autoregen.py /usr/local/bin/autoregen.py
# Build and install autoconf and automake
# Automake depends on autoconf, which is built and installed first
RUN set -eux; \
\
savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \
apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
gzip \
m4 \
tar \
wget \
; \
rm -r /var/lib/apt/lists/*; \
\
builddir="$(mktemp -d)"; \
\
cd "${builddir}"; \
AUTOCONF_VERSION_SHORT="$(echo $AUTOCONF_VERSION | awk -F. '{ print $1 "." $2 }')"; \
wget -q "https://ftp.gnu.org/gnu/autoconf/autoconf-${AUTOCONF_VERSION}.tar.gz"; \
tar xf "autoconf-${AUTOCONF_VERSION}.tar.gz"; \
cd "autoconf-${AUTOCONF_VERSION}"; \
./configure --program-suffix="-${AUTOCONF_VERSION}"; \
make; \
make install; \
cd .. ;\
rm -rf autoconf*; \
cd /usr/local/bin; \
for f in autoconf autoheader autom4te autoreconf autoscan autoupdate ifnames; do \
ln -sv "$f-$AUTOCONF_VERSION" "$f"; \
[ ! "$AUTOCONF_VERSION" = "$AUTOCONF_VERSION_SHORT" ] && \
ln -sv "$f-$AUTOCONF_VERSION" "$f-$AUTOCONF_VERSION_SHORT"; \
done; \
\
cd "${builddir}"; \
AUTOMAKE_VERSION_SHORT="$(echo $AUTOMAKE_VERSION | awk -F. '{ print $1 "." $2 }')"; \
wget -q "https://ftp.gnu.org/gnu/automake/automake-${AUTOMAKE_VERSION}.tar.gz"; \
tar xf "automake-${AUTOMAKE_VERSION}.tar.gz"; \
cd "automake-${AUTOMAKE_VERSION}"; \
./configure --program-suffix="-${AUTOMAKE_VERSION}"; \
make; \
make install; \
cd ..; \
rm -rf automake*; \
cd /usr/local/bin; \
for f in aclocal automake; do \
ln -sv "$f-$AUTOMAKE_VERSION" "$f"; \
[ ! "$AUTOMAKE_VERSION" = "$AUTOMAKE_VERSION_SHORT" ] && \
ln -sv "$f-$AUTOMAKE_VERSION" "$f-$AUTOMAKE_VERSION_SHORT"; \
done; \
\
rm -rf "${builddir}"; \
\
apt-mark auto '.*' > /dev/null; \
[ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false

View File

@@ -0,0 +1,146 @@
#!/usr/bin/env python3
# This script helps to regenerate files managed by autotools and
# autogen in binutils-gdb and gcc repositories.
# It can be used by buildbots to check that the current repository
# contents has been updated correctly, and by developers to update
# such files as expected.
import os
import shutil
import subprocess
from pathlib import Path
# On Gentoo, vanilla unpatched autotools are packaged separately.
# We place the vanilla names first as we want to prefer those if both exist.
AUTOCONF_NAMES = ["autoconf-vanilla-2.69", "autoconf-2.69", "autoconf"]
AUTOMAKE_NAMES = ["automake-vanilla-1.15", "automake-1.15.1", "automake"]
ACLOCAL_NAMES = ["aclocal-vanilla-1.15", "aclocal-1.15.1", "aclocal"]
AUTOHEADER_NAMES = ["autoheader-vanilla-2.69", "autoheader-2.69", "autoheader"]
AUTORECONF_NAMES = ["autoreconf-vanilla-2.69", "autoreconf-2.69", "autoreconf"]
# Pick the first for each list that exists on this system.
AUTOCONF_BIN = next(name for name in AUTOCONF_NAMES if shutil.which(name))
AUTOMAKE_BIN = next(name for name in AUTOMAKE_NAMES if shutil.which(name))
ACLOCAL_BIN = next(name for name in ACLOCAL_NAMES if shutil.which(name))
AUTOHEADER_BIN = next(name for name in AUTOHEADER_NAMES if shutil.which(name))
AUTORECONF_BIN = next(name for name in AUTORECONF_NAMES if shutil.which(name))
AUTOGEN_BIN = "autogen"
# autoconf-wrapper and automake-wrapper from Gentoo look at this environment variable.
# It's harmless to set it on other systems though.
EXTRA_ENV = {
"WANT_AUTOCONF": AUTOCONF_BIN.split("-", 1)[1] if "-" in AUTOCONF_BIN else "",
"WANT_AUTOMAKE": AUTOMAKE_BIN.split("-", 1)[1] if "-" in AUTOMAKE_BIN else "",
"AUTOCONF": AUTOCONF_BIN,
"ACLOCAL": ACLOCAL_BIN,
"AUTOMAKE": AUTOMAKE_BIN,
"AUTOGEN": AUTOGEN_BIN,
}
ENV = os.environ.copy()
ENV.update(EXTRA_ENV)
# Directories we should skip entirely because they're vendored or have different
# autotools versions.
SKIP_DIRS = [
# readline and minizip are maintained with different autotools versions
"readline",
"minizip",
]
MANUAL_CONF_DIRS = [
".",
# autoreconf does not update aclocal.m4
"fixincludes",
]
# Run the shell command CMD.
#
# Print the command on stdout prior to running it.
def run_shell(cmd: str):
print(f"+ {cmd}", flush=True)
res = subprocess.run(
f"{cmd}",
shell=True,
encoding="utf8",
env=ENV,
)
res.check_returncode()
def regenerate_with_autoreconf():
run_shell(f"{AUTORECONF_BIN} -f")
def regenerate_with_autogen():
run_shell(f"{AUTOGEN_BIN} Makefile.def")
def regenerate_manually():
configure_lines = open("configure.ac").read().splitlines()
if folder.stem == "fixincludes" or any(
True for line in configure_lines if line.startswith("AC_CONFIG_MACRO_DIR")
):
include_arg = ""
include_arg2 = ""
if (folder / ".." / "config").is_dir():
include_arg = "-I../config"
if folder.stem == "fixincludes":
include_arg = "-I.."
include_arg2 = "-I../config"
# aclocal does not support the -f short option for force
run_shell(f"{ACLOCAL_BIN} --force {include_arg} {include_arg2}")
if (folder / "config.in").is_file() or any(
True for line in configure_lines if line.startswith("AC_CONFIG_HEADERS")
):
run_shell(f"{AUTOHEADER_BIN} -f")
# apparently automake is somehow unstable -> skip it for gotools
if any(
True for line in configure_lines if line.startswith("AM_INIT_AUTOMAKE")
) and not str(folder).endswith("gotools"):
run_shell(f"{AUTOMAKE_BIN} -f")
run_shell(f"{AUTOCONF_BIN} -f")
run_shell(f"{AUTOCONF_BIN} --version")
run_shell(f"{AUTOMAKE_BIN} --version")
run_shell(f"{ACLOCAL_BIN} --version")
run_shell(f"{AUTOHEADER_BIN} --version")
print(f"Extra environment: {EXTRA_ENV}", flush=True)
config_folders: list[Path] = []
autogen_folders: list[Path] = []
repo_root = Path.cwd()
for root, _, files in os.walk("."):
for file in files:
if file == "configure.ac":
config_folders.append(Path(root).resolve())
if file == "Makefile.tpl":
autogen_folders.append(Path(root).resolve())
for folder in sorted(autogen_folders):
print(f"Entering directory {folder}", flush=True)
os.chdir(folder)
regenerate_with_autogen()
for folder in sorted(config_folders):
if folder.stem in SKIP_DIRS:
print(f"Skipping directory {folder}", flush=True)
continue
print(f"Entering directory {folder}", flush=True)
os.chdir(folder)
if str(folder.relative_to(repo_root)) in MANUAL_CONF_DIRS:
regenerate_manually()
else:
regenerate_with_autoreconf()

View File

@@ -0,0 +1,104 @@
#!/usr/bin/env bash
#
# Build a container using buildah
#
set -euo pipefail
usage() {
cat <<EOF
Usage: build-image.sh -d <directory> -t <tag> [-e timestamp] [-- buildah-args...]
Options:
-d, --dir <path> Directory with the Containerfile (required).
-t, --tag <tag> Tag to apply to the built image (required).
-e, --epoch <ts> Set the "created" timestamp for the built image to this number of seconds since the epoch (optional).
Default is to use the timestamp of the current commit.
Needs buildah 1.41 or newer.
-h, --help Show this help message and exit.
All arguments after a double-dash (--) are forwarded unchanged to 'buildah'.
Example:
./build-image.sh -d src -t v1.0 -- --layers --no-cache
EOF
exit 1
}
DIR=""
TAG=""
EXTRA_ARGS=()
while (( "$#" )); do
case "$1" in
-d|--dir)
if [[ -n "${2-}" ]]; then
DIR="$2"
shift 2
else
echo "error: --dir requires a value" >&2
exit 1
fi
;;
-t|--tag)
if [[ -n "${2-}" ]]; then
TAG="$2"
shift 2
else
echo "error: --tag requires a value" >&2
exit 1
fi
;;
-e|--epoch)
if [[ -n "${2-}" ]]; then
SOURCE_DATE_EPOCH="$2"
shift 2
else
echo "error: --source-date-epoch requires a value" >&2
exit 1
fi
;;
-h|--help)
usage
;;
--)
shift
EXTRA_ARGS+=("$@")
break
;;
*)
echo "error: unknown option '$1'" >&2
usage
;;
esac
done
if [[ -z "$DIR" ]]; then
echo "error: directory (-d/--dir) is required" >&2
usage
fi
if [[ -z "$TAG" ]]; then
echo "error: Tag (-t/--tag) is required." >&2
usage
fi
if [[ ! -e "${DIR}/Containerfile" ]]; then
echo "error: '${DIR}/Containerfile' does not exist." >&2
usage
fi
CONTAINER="$(basename "$DIR")"
IMAGE_TAG="${CONTAINER}:${TAG}"
if [[ -z "${SOURCE_DATE_EPOCH-}" ]]; then
SCRIPT_DIR="$(dirname "$0")"
SOURCE_DATE_EPOCH="$(cd "${SCRIPT_DIR}" && git log -1 --pretty=%ct)"
fi
export SOURCE_DATE_EPOCH
buildah build \
-f "${DIR}/Containerfile" \
-t "$IMAGE_TAG" \
"${EXTRA_ARGS[@]}" \
"$DIR"