Skip to content
Draft
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
175 changes: 175 additions & 0 deletions .ci/check-debian-packaging.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#!/bin/sh
# Static validation of the debian/ packaging metadata in this repository.
#
# A real package build compiles all of OpenModelica and takes hours, so the
# nightly Jenkins job is the only place the packaging is exercised end to end.
# This script checks what can be checked in seconds: that the control files
# parse, that debhelper accepts the declared compatibility level on this
# distribution, that debian/rules only calls helpers that still exist and only
# passes options that still exist at that compat level, and that every
# debian/<package>.<helper> file belongs to a binary package we build.
#
# It does NOT catch a .install glob that stopped matching because upstream moved
# a file; only a real build does that.
#
# Run it from the top of the repository, inside a distribution we ship for:
#
# docker run --rm -v "$PWD:/src" -w /src ubuntu:24.04 sh -c \
# 'apt-get update -qq && apt-get install -qy --no-install-recommends \
# debhelper dpkg-dev && .ci/check-debian-packaging.sh'

set -eu

# Keep in sync with the compat level declared in the control files.
EXPECTED_COMPAT=13

TREES="debian OMPlot/debian OMOptim/debian OpenModelica-doc/debian"

# debhelper config files named debian/<binary package>.<suffix>. A typo in the
# package part makes debhelper ignore the file without a word, and the package
# then ships empty, so the package name has to exist in debian/control.
PKG_SUFFIXES="install docs dirs links examples manpages menu menu-method info
init service tmpfiles sharedmimeinfo lintian-overrides preinst postinst prerm
postrm triggers README.Debian NEWS"

# Command/option combinations debhelper has removed. The compat level each was
# dropped in is listed in debhelper-compat-upgrade-checklist(7); add a line here
# when a helper we call loses an option we pass.
# Fields: helper:option:compat level that removed it:what to use instead
removed_usage() {
cat <<'REMOVED'
dh_install:--list-missing:12:dh_missing --list-missing
dh_install:--fail-missing:12:dh_missing --fail-missing
dh_clean:-k:12:dh_prep
dh_installinit:--no-restart-on-upgrade:12:--no-stop-on-upgrade
REMOVED
}

status=0
fail() { echo " FAIL: $*" >&2; status=1; }
ok() { echo " ok: $*"; }

command -v dh_assistant >/dev/null 2>&1 || {
echo "dh_assistant not found; this script needs debhelper >= 13.5" >&2
exit 2
}

scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT

echo "debhelper $(dpkg-query -f '${Version}' -W debhelper) on $(. /etc/os-release; echo "$PRETTY_NAME")"

for tree in $TREES; do
echo "== $tree"
work="$scratch/$(echo "$tree" | tr / _)"
mkdir -p "$work"
cp -a "$tree" "$work/debian"

# debian/changelog is a template. The source-package job fills it in the same
# way (see update-source-repo.py and make-packages.py in the apt-build repo).
sed -i -e 's/@REV@/1.0.0~dev-0-g0000000/' -e 's/@DISTS@/unstable/' \
-e "s/@TIME@/$(date -R)/" "$work/debian/changelog"

if [ -e "$work/debian/compat" ]; then
fail "debian/compat still exists; the compat level belongs in Build-Depends: debhelper-compat"
else
ok "no stale debian/compat"
fi

if ! (
cd "$work" || exit 1

if ! changelog=$(dpkg-parsechangelog 2>&1); then
printf '%s\n' "$changelog" >&2
echo " FAIL: debian/changelog does not parse" >&2
exit 1
fi
echo " ok: changelog parses ($(printf '%s\n' "$changelog" | sed -n 's/^Source: //p')" \
"$(printf '%s\n' "$changelog" | sed -n 's/^Version: //p'))"

if ! perl -MDpkg::Control::Info -e '
my $c = Dpkg::Control::Info->new("debian/control");
print $_->{Package}, "\n" for $c->get_packages();
' > packages.txt 2> control.err; then
cat control.err >&2
echo " FAIL: debian/control does not parse" >&2
exit 1
fi
if ! [ -s packages.txt ]; then
echo " FAIL: debian/control declares no binary packages" >&2
exit 1
fi
echo " ok: control parses ($(wc -l < packages.txt | tr -d ' ') binary packages)"

# The check that catches a mis-declared compat level: debhelper accepts only
# "debhelper-compat (= N)", and errors out if this distribution's debhelper
# does not support N.
if ! compat=$(dh_assistant active-compat-level 2>&1); then
printf '%s\n' "$compat" >&2
echo " FAIL: debhelper rejects the declared compatibility level" >&2
exit 1
fi
compat=$(printf '%s\n' "$compat" | sed -n 's/.*"active-compat-level":\([0-9]*\).*/\1/p')
if [ "$compat" != "$EXPECTED_COMPAT" ]; then
echo " FAIL: active compat level is '$compat', expected $EXPECTED_COMPAT" >&2
exit 1
fi
echo " ok: compat level $compat"
); then
status=1
continue
fi

packages="$work/packages.txt"

# Every helper debian/rules calls has to still ship in this debhelper.
sed -e 's/#.*//' "$tree/rules" > "$work/rules.uncommented"
missing=""
for helper in $(grep -o 'dh_[a-z0-9_]*' "$work/rules.uncommented" | sort -u); do
command -v "$helper" >/dev/null 2>&1 || missing="$missing $helper"
done
if [ -n "$missing" ]; then
fail "debian/rules calls helpers that no longer exist:$missing"
else
ok "every dh_* helper called by debian/rules exists"
fi

# ... and must not pass options removed at or below our compat level.
removed=""
removed_usage > "$work/removed-usage"
while IFS=: read -r cmd opt gone use; do
[ -n "$cmd" ] || continue
[ "$EXPECTED_COMPAT" -ge "$gone" ] || continue
if grep -q -- "$cmd[^&|;]*[[:space:]]$opt\\b" "$work/rules.uncommented"; then
removed="$removed
$cmd $opt (gone in compat $gone; use: $use)"
fi
done < "$work/removed-usage"
if [ -n "$removed" ]; then
fail "debian/rules uses debhelper options removed at compat $EXPECTED_COMPAT:$removed"
else
ok "no debhelper options removed at compat $EXPECTED_COMPAT"
fi

orphans=""
for f in "$tree"/*; do
[ -f "$f" ] || continue
base=${f##*/}
for suffix in $PKG_SUFFIXES; do
case "$base" in
*".$suffix")
pkg=${base%".$suffix"}
grep -qx "$pkg" "$packages" || orphans="$orphans $base"
;;
esac
done
done
if [ -n "$orphans" ]; then
fail "config files naming a package that is not in debian/control:$orphans"
else
ok "every debian/<package>.<helper> file matches a binary package"
fi
done

[ $status -eq 0 ] && echo "all checks passed"
exit $status
78 changes: 78 additions & 0 deletions .ci/check-rpm-spec.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/bin/sh
# Static validation of rpm/SPECS/openmodelica.spec.tpl.
#
# The template is not a valid spec file on its own: the RPM job in the apt-build
# repository substitutes a set of upper-case placeholders into it before calling
# rpmbuild (see the rpmInfo map in apt-build's Jenkinsfile). This script does
# the same substitution with representative values and then asks rpm itself to
# parse the result, which catches the syntax mistakes that have broken nightly
# RPM builds before: a missing %, an empty %define, a stray comment.
#
# It does not build anything, so it says nothing about BuildRequires being
# installable or the %files list matching what the build produces.
#
# Run it from the top of the repository, inside a distribution we ship for:
#
# docker run --rm -v "$PWD:/src" -w /src fedora:43 sh -c \
# 'dnf install -y rpm-build >/dev/null && .ci/check-rpm-spec.sh'

set -eu

TEMPLATE=rpm/SPECS/openmodelica.spec.tpl

# Representative values, in the order the Jenkins job applies them. They only
# have to be shaped like the real thing; the point is to parse the spec.
NAME=openmodelica-nightly
DEBVERSION=1.28.0~dev-489-gda9d1ce
RPMVERSION=1.28.0~dev~489~gda9d1ce
CONFIGUREFLAGS=
DOCUMENTATIONVERSION=latest
PATCHCMDS=
PATCHES=
PRIORITY=1018000
PRIVATELIBS='lib.*Modelica.*|lib[oO][mM][^n].*[.]so.*|libklu[.]so.*|libsundials.*'
RELEASENUM=1
BRANCH=nightly
DATE=$(LC_ALL=C date "+%a %b %d %Y")

command -v rpmspec >/dev/null 2>&1 || {
echo "rpmspec not found; install rpm-build" >&2
exit 2
}

scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
spec="$scratch/openmodelica.spec"

# '%' is safe as the sed delimiter: none of the values above contain one.
sed -e "s%NAME%$NAME%g" \
-e "s%DEBVERSION%$DEBVERSION%g" \
-e "s%RPMVERSION%$RPMVERSION%g" \
-e "s%CONFIGUREFLAGS%$CONFIGUREFLAGS%g" \
-e "s%DOCUMENTATIONVERSION%$DOCUMENTATIONVERSION%g" \
-e "s%PATCHCMDS%$PATCHCMDS%g" \
-e "s%PATCHES%$PATCHES%g" \
-e "s%PRIORITY%$PRIORITY%g" \
-e "s%PRIVATELIBS%$PRIVATELIBS%g" \
-e "s%RELEASENUM%$RELEASENUM%g" \
-e "s%BRANCH%$BRANCH%g" \
-e "s%DATE%$DATE%g" \
"$TEMPLATE" > "$spec"

echo "rpm $(rpm --version | awk '{print $3}') on $(. /etc/os-release; echo "$PRETTY_NAME")"

if ! out=$(rpmspec -P "$spec" 2>&1 >/dev/null); then
printf '%s\n' "$out" >&2
echo " FAIL: $TEMPLATE does not parse" >&2
exit 1
fi
[ -z "$out" ] || printf '%s\n' "$out"
echo " ok: template parses"

nevr=$(rpmspec -q --srpm --qf '%{name} %{version}-%{release}\n' "$spec" 2>/dev/null)
echo " ok: source package would be $nevr"

subpkgs=$(rpmspec -q --qf '%{name}\n' "$spec" 2>/dev/null | sort -u | tr '\n' ' ')
echo " ok: binary packages: $subpkgs"

echo "all checks passed"
17 changes: 17 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Keep the actions used by the workflows in .github/workflows up to date.
#
# They are pinned to commit SHAs, so without this they silently stay on whatever
# was current when the workflow was written. Quarterly is often enough for a
# repository whose CI is three files, and the group means it arrives as one pull
# request rather than one per action.
version: 2
updates:
- package-ecosystem: github-actions
# For github-actions this means .github/workflows, not a subdirectory.
directory: /
schedule:
interval: quarterly
groups:
actions:
patterns:
- "*"
110 changes: 110 additions & 0 deletions .github/workflows/build-deb.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Full build of OpenModelica plus the Debian packages, the way the nightly
# Jenkins job does it: configure/make (autoconf) from a git checkout, then
# dpkg-buildpackage using the debian/ directory of this repository.
#
# This is the only job that can catch a .install glob that stopped matching
# because upstream moved or dropped a file -- the failure that breaks the
# nightly build. It compiles all of OpenModelica, so it takes hours. The fast
# metadata-only checks live in packaging.yml.
#
# Uses the Autoconf + Makefile build; to be switched to the CMake build later.
name: full deb build

on:
workflow_dispatch:
inputs:
openmodelica-ref:
description: OpenModelica git ref to build
default: master
pull_request:
paths:
- 'debian/**'
- '.github/workflows/build-deb.yml'

permissions:
contents: read

jobs:
build:
# Only ubuntu-latest: Jenkins covers the rest of the distribution matrix.
runs-on: ubuntu-latest
timeout-minutes: 360

steps:
- name: Check out the build scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: buildscripts

- name: Check out OpenModelica
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: OpenModelica/OpenModelica
ref: ${{ inputs.openmodelica-ref || 'master' }}
submodules: recursive
# The package version comes from git describe, so the tags and the
# history behind them have to be there.
fetch-depth: 0
path: openmodelica

- name: Free up disk space
run: |
# A full OpenModelica build does not fit next to the preinstalled
# toolchains on a hosted runner.
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \
/usr/local/share/boost "$AGENT_TOOLSDIRECTORY" || true
df -h /

- name: Install build dependencies
run: |
sudo apt-get update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install -qy \
--no-install-recommends build-essential devscripts equivs fakeroot
# Same approach as the build-deps images (Dockerfile.deb.template in
# the apt-build repository): install exactly what debian/control asks
# for, so a missing or renamed build dependency fails here.
sudo mk-build-deps --install --remove \
--tool 'apt-get -y --no-install-recommends' \
buildscripts/debian/control

- name: Assemble the source tree
working-directory: openmodelica
run: |
# Version string exactly as update-source-repo.py builds it:
# v1.28.0-dev-489-gda9d1ce -> 1.28.0~dev-489-gda9d1ce
version=$(git describe --abbrev=7 --tags --match='v*.*.*' 2>/dev/null |
sed -e 's/^v//' -e 's/-/~/') || true
if [ -z "$version" ]; then
version="0.0.0~ci-$(git rev-parse --short HEAD)"
echo "git describe found no tag; using $version"
fi
echo "Building openmodelica $version"

rm -rf debian
cp -a ../buildscripts/debian debian
sed -i -e "s/@REV@/$version/" -e "s/@TIME@/$(date -R)/" debian/changelog
head -1 debian/changelog

- name: Build the packages
working-directory: openmodelica
run: dpkg-buildpackage -rfakeroot -b -us -uc -j"$(nproc)"

- name: Show what was built
if: always()
run: |
ls -la *.deb || true
for deb in *.deb; do
[ -e "$deb" ] || continue
echo "=== $deb"
dpkg-deb -I "$deb" | sed -n 's/^ //p' | grep -E '^(Package|Version|Architecture|Depends):' || true
dpkg-deb -c "$deb" | wc -l | xargs echo " files:"
done

- name: Upload packages
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: debs
path: '*.deb'
if-no-files-found: warn
retention-days: 7
Loading