Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions build-in-container-inner.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/bin/bash
set -e

# Configuration via environment variables:
# PROJECT, BUILD_TYPE, EXPLICIT_ROLE, BUILD_NUMBER, EXPLICIT_VERSION

BASEDIR=/home/builder/build
export BASEDIR
export AUTOBUILD_PATH="$BASEDIR/buildscripts"

mkdir -p "$BASEDIR"

# Bind-mounted directories may be owned by the host user's UID.
# Fix ownership so builder can write to them.
sudo chown -R "$(id -u):$(id -g)" "$HOME/.cache" /output

# Prevent git "dubious ownership" errors
git config --global --add safe.directory '*'

# === Sync source repos ===
repos="buildscripts core masterfiles"
if [ "$PROJECT" = "nova" ]; then
repos="$repos enterprise nova mission-portal"
fi

for repo in $repos; do
src="/srv/source/$repo"
# Use rsync -aL to follow symlinks during copy.
# The source dir may use symlinks (e.g., core -> cfengine/core/).
# -L resolves them at copy time, so the destination gets real files
# regardless of the host directory layout.
# Exclude acceptance test workdirs — they contain broken symlinks left
# over from previous test runs and are not needed for building.
if [ -d "$src" ] || [ -L "$src" ]; then
echo "Syncing $repo..."
sudo rsync -aL --exclude='config.cache' --exclude='workdir' --chown="$(id -u):$(id -g)" "$src/" "$BASEDIR/$repo/"
else
echo "ERROR: Required repository $repo not found" >&2
exit 1
fi
done

install_mission_portal_deps() (
set -e

if [ -f "$BASEDIR/mission-portal/public/scripts/package.json" ]; then
echo "Installing npm dependencies..."
npm ci --prefix "$BASEDIR/mission-portal/public/scripts/"
echo "Building react components..."
npm run build --prefix "$BASEDIR/mission-portal/public/scripts/"
rm -rf "$BASEDIR/mission-portal/public/scripts/node_modules"
fi

if [ -f "$BASEDIR/mission-portal/composer.json" ]; then
echo "Installing Mission Portal PHP dependencies..."
(cd "$BASEDIR/mission-portal" && php /usr/bin/composer.phar install --no-dev --ignore-platform-reqs)
fi

if [ -f "$BASEDIR/nova/api/http/composer.json" ]; then
echo "Installing Nova API PHP dependencies..."
(cd "$BASEDIR/nova/api/http" && php /usr/bin/composer.phar install --no-dev --ignore-platform-reqs)
fi

if [ -f "$BASEDIR/mission-portal/public/themes/default/bootstrap/cfengine_theme.less" ]; then
echo "Compiling Mission Portal styles..."
mkdir -p "$BASEDIR/mission-portal/public/themes/default/bootstrap/compiled/css"
(cd "$BASEDIR/mission-portal/public/themes/default/bootstrap" &&
lessc --compress ./cfengine_theme.less ./compiled/css/cfengine.less.css)
fi

if [ -f "$BASEDIR/mission-portal/ldap/composer.json" ]; then
echo "Installing LDAP API PHP dependencies..."
(cd "$BASEDIR/mission-portal/ldap" && php /usr/bin/composer.phar install --no-dev --ignore-platform-reqs)
fi
)

# === Step runner with failure reporting ===
# Disable set -e so we can capture exit codes and report which step failed.
set +e
run_step() {
local name="$1"
shift
echo "=== Running $name ==="
"$@"
local rc=$?
if [ $rc -ne 0 ]; then
echo ""
echo "=== FAILED: $name (exit code $rc) ==="
exit $rc
fi
}

# === Build steps ===
run_step "01-autogen" "$BASEDIR/buildscripts/build-scripts/autogen"
run_step "02-install-dependencies" "$BASEDIR/buildscripts/build-scripts/install-dependencies"
if [ "$EXPLICIT_ROLE" = "hub" ]; then
run_step "03-mission-portal-deps" install_mission_portal_deps
fi
run_step "04-configure" "$BASEDIR/buildscripts/build-scripts/configure"
run_step "05-compile" "$BASEDIR/buildscripts/build-scripts/compile"
run_step "06-package" "$BASEDIR/buildscripts/build-scripts/package"
Comment on lines +93 to +101
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we should rename build-scripts folder to steps / build-steps 😅.


# === Copy output packages ===
# Packages are created under $BASEDIR/<project>/ by dpkg-buildpackage / rpmbuild.
# Exclude deps-packaging to avoid copying dependency packages.
find "$BASEDIR" -maxdepth 4 \
-path "$BASEDIR/buildscripts/deps-packaging" -prune -o \
\( -name '*.deb' -o -name '*.rpm' -o -name '*.pkg.tar.gz' \) -print \
-exec cp {} /output/ \;

echo ""
echo "=== Build complete ==="
ls -lh /output/
117 changes: 117 additions & 0 deletions build-in-container.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# build-in-container

Build CFEngine packages inside Docker containers using build scripts. Requires
only Docker and Python 3 on the host.

## Quick start

```bash
# Build a community agent .deb for Ubuntu 22
./build-in-container.py --platform ubuntu-22 --project community --role agent --build-type DEBUG

# Build a nova hub release package for Debian 12
./build-in-container.py --platform debian-12 --project nova --role hub --build-type RELEASE
```

In the examples above, we run the script from inside `buildscripts/` (with
`buildscripts` as our current working directory). This is not required — if not
specified, defaults will:

- Look for sources relative to the script (parent directory of
`build-in-container.py`).
- Place cache files in the user's home directory
(`~/.cache/cfengine/buildscripts`).
- Use the current working directory for output packages (`./output/`).

## Usage

```
./build-in-container.py --platform PLATFORM --project PROJECT --role ROLE --build-type TYPE [OPTIONS]
```

### Required arguments

| Option | Description |
|--------------------|-------------------------------------------------|
| `--platform` | Target platform (e.g. `ubuntu-22`, `debian-12`) |
| `--project` | `community` or `nova` |
| `--role` | `agent` or `hub` |
| `--build-type` | `DEBUG` or `RELEASE` |

### Optional arguments

| Option | Default | Description |
|--------------------|----------------------------------|-------------------------------------------------------------|
| `--output-dir` | `./output` | Where to write output packages |
| `--cache-dir` | `~/.cache/cfengine/buildscripts` | Dependency cache directory |
| `--build-number` | `1` | Build number for package versioning |
| `--version` | auto | Override version string |
| `--rebuild-image` | | Force rebuild of Docker image (bypasses Docker layer cache) |
| `--shell` | | Drop into a bash shell inside the container for debugging |
| `--list-platforms` | | List available platforms and exit |
| `--source-dir` | parent of `buildscripts/` | Root directory containing repos |

## Supported platforms

| Name | Base image |
|-------------|----------------|
| `ubuntu-20` | `ubuntu:20.04` |
| `ubuntu-22` | `ubuntu:22.04` |
| `ubuntu-24` | `ubuntu:24.04` |
| `debian-11` | `debian:11` |
| `debian-12` | `debian:12` |

Adding a new Debian/Ubuntu platform requires only a new entry in the `PLATFORMS`
dict in `build-in-container.py`. Adding a non-debian based platform (e.g.,
RHEL/CentOS) requires a new `container/Dockerfile.rhel` plus platform entries.

## How it works

The system has three components:

1. **`build-in-container.py`** (Python) -- the orchestrator that runs on the host.
Parses arguments, builds the Docker image, and launches the container with
the correct mounts and environment variables.

2. **`build-in-container-inner.sh`** (Bash) -- runs inside the container. Copies
source repos from the read-only mount, then calls the existing build scripts
in order.

3. **`container/Dockerfile.debian`** -- parameterized Dockerfile shared by all
Debian/Ubuntu platforms via a `BASE_IMAGE` build arg.

### Container mounts

| Host path | Container path | Mode | Purpose |
|------------------------------------------|-------------------------------------------|------------|---------------------------------------|
| Source repos (parent of `buildscripts/`) | `/srv/source` | read-only | Protects host repos from modification |
| `~/.cache/cfengine/buildscripts/` | `/home/builder/.cache/buildscripts_cache` | read-write | Dependency cache shared across builds |
| `./output/` | `/output` | read-write | Output packages copied here |

### Build steps

The inner script runs these steps in order:

1. **autogen** -- runs `autogen.sh` in each repo
2. **install-dependencies** -- builds and installs bundled dependencies
3. **mission-portal-deps** -- (hub only) installs PHP/npm/LESS assets
4. **configure** -- runs `./configure` with platform-appropriate flags
5. **compile** -- compiles and installs to the dist tree
6. **package** -- creates `.deb` or `.rpm` packages

## Docker image management

The Docker image is tagged `cfengine-builder-{platform}` and rebuilt
automatically when the Dockerfile changes (tracked via a content hash stored as
an image label). Use `--rebuild-image` to force a full rebuild bypassing the
Docker layer cache (useful when upstream packages change).

## Debugging

```bash
# Drop into a shell inside the container
./build-in-container.py --platform ubuntu-22 --project community --role agent --build-type DEBUG --shell
```

The shell session has the same mounts and environment as a build run. The
container is ephemeral (`--rm`), so any changes are lost on exit.
Loading
Loading