Skip to content

Build

Build #783

Workflow file for this run

name: Build
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
concurrency:
group: build
cancel-in-progress: false
jobs:
build:
name: Build
runs-on: ubuntu-latest
permissions:
actions: write
packages: write
contents: read
steps:
-
name: Get latest version
run: |
#!/usr/bin/env bash
set -Eeuo pipefail
api="https://api.access.redhat.com/management/v1"
arch="x86_64"
limit=100
initial_offset=3500
resp=$(
curl -fsS \
"https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token" \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "client_id=rhsm-api" \
--data-urlencode "refresh_token=${{ secrets.REDHAT_TOKEN }}"
)
token=$(jq -er '.access_token' <<< "$resp")
version=""
release=""
checksum=""
distro=""
for major in {15..7}; do
found="N"
for variant in "-beta" "-eus" ""; do
candidate="rhel-${major}-for-x86_64-appstream${variant}-rpms"
valid="N"
echo "Checking $candidate..."
for start_offset in "$initial_offset" 0; do
if [ "$start_offset" -eq 0 ]; then
[ "$valid" = "Y" ] || break
echo "Retrying $candidate from offset 0..."
fi
offset=$start_offset
while :; do
[[ "$offset" != "$initial_offset" ]] && echo "Offset $offset"
resp=$(
curl -sS \
-H "Authorization: Bearer $token" \
-H "Accept: application/json" \
"$api/packages/cset/$candidate/arch/$arch?limit=$limit&offset=$offset&filter=latest"
)
if jq -e '.error' >/dev/null 2>&1 <<< "$resp"; then
break
fi
if ! jq -e '.body | type == "array"' >/dev/null 2>&1 <<< "$resp"; then
echo "Unexpected response for $candidate:"
echo "$resp"
break
fi
valid="Y"
match=$(
jq -c \
'first(.body[]? | select(.name == "virtio-win")) // empty' \
<<< "$resp"
)
if [ -n "$match" ]; then
version=$(jq -er '.version' <<< "$match")
release=$(jq -er '.release' <<< "$match")
checksum=$(jq -er '.checksum' <<< "$match")
distro=$candidate
found="Y"
break
fi
count=$(jq -r '.body | length' <<< "$resp")
if [ "$count" -lt "$limit" ]; then
break
fi
offset=$((offset + limit))
sleep 0.2
done
[ "$found" = "Y" ] && break
done
[ "$found" = "Y" ] && break
done
[ "$found" = "Y" ] && break
done
if [ -z "$version" ] || [ -z "$release" ] || [ -z "$checksum" ]; then
echo "ERROR: Could not find virtio-win in any RHEL AppStream content set." >&2
exit 1
fi
pkg="$version-$release"
latest_tag="${pkg%.el*}"
if [ -z "$latest_tag" ]; then
echo "ERROR: Tag not found." >&2
exit 1
fi
echo "Selected content set: $distro"
echo "Found tag: \"$latest_tag\""
resp=$(
curl -fsS \
-H "Authorization: Bearer $token" \
-H "Accept: application/json" \
"$api/packages/$checksum/download"
)
download=$(jq -er '.body.href' <<< "$resp")
base=${latest_tag%%-*}
{
echo "latest_tag=$latest_tag"
echo "latest_base=$base"
echo "latest_download=$download"
} >> "$GITHUB_ENV"
-
name: Check if the tag exists locally
uses: action-pack/tag-exists@v1
id: checkTag
with:
tag: 'v${{ env.latest_tag }}'
-
name: Finish when found
run: |
#!/usr/bin/env bash
set -Eeuo pipefail
echo "exists=${{ steps.checkTag.outputs.exists }}" >> "$GITHUB_ENV"
-
name: Download package
if: env.exists == 'false'
id: rocky
run: |
#!/usr/bin/env bash
set -Eeuo pipefail
wget "${{ env.latest_download }}" -O /tmp/latest.rpm -q
-
name: Process package
if: env.exists == 'false'
id: process
run: |
#!/usr/bin/env bash
set -Eeuo pipefail
sudo apt-get update
sudo apt-get install -y rdfind genisoimage libarchive-tools rpm unzip
rpm -qp --changelog /tmp/latest.rpm > /tmp/rpm-changelog
rm -rf /tmp/rpm /tmp/iso
mkdir -p /tmp/rpm /tmp/iso
cd /tmp/rpm
rpm2cpio /tmp/latest.rpm | cpio -idm
cp -L \
/tmp/rpm/usr/share/virtio-win/virtio-win.iso \
/tmp/latest.iso
bsdtar -xf /tmp/latest.iso -C /tmp/iso
-
name: Strip unnecessary bloat
if: env.exists == 'false'
id: strip
run: |
#!/usr/bin/env bash
set -Eeuo pipefail
# Remove bloat.
sudo rm -f /tmp/iso/winfsp-*
# Remove tools for other platforms.
sudo rm -rf \
/tmp/iso/i386 \
/tmp/iso/tools \
/tmp/iso/guest-agent
sudo rm -f \
/tmp/iso/virtio-win-gt-x86.msi \
/tmp/iso/virtio-win-guest-tools.exe
# Remove test and bridge utilities.
if [ -d /tmp/iso/viosock ]; then
sudo find /tmp/iso/viosock \
-type f \
\( \
-iname 'viosock-test.exe' -o \
-iname 'viosocklib-test.exe' -o \
-iname 'vstbridge.exe' \
\) \
-delete
fi
# Remove debug symbols.
sudo find /tmp/iso -type f -iname '*.pdb' -delete
# Remove modern drivers for other architectures.
sudo find /tmp/iso \
-type d \
\( -iname x86 -o -iname arm64 \) \
-prune \
-exec rm -rf {} +
# Remove empty directories.
sudo find /tmp/iso -depth -type d -empty -delete
-
name: Add legacy drivers
if: env.exists == 'false'
id: legacy
run: |
#!/usr/bin/env bash
set -Eeuo pipefail
url="https://github.com/qemus/virtiso-whql/releases/download/v1.9.40-0/legacy.zip"
wget "$url" -O /tmp/legacy.zip -q
sudo unzip -o /tmp/legacy.zip -d /tmp/iso >/dev/null
# Replace any symlinks from the archive with normal files or directories.
sudo bash -Eeuo pipefail <<'EOF'
while IFS= read -r -d '' link; do
target=$(readlink -f -- "$link")
if [ -z "$target" ] || [ ! -e "$target" ]; then
echo "ERROR: Broken symlink in legacy archive: $link" >&2
exit 1
fi
case "$target" in
/tmp/iso/*) ;;
*)
echo "ERROR: Symlink points outside the ISO tree: $link -> $target" >&2
exit 1
;;
esac
replacement="${link}.resolved"
rm -rf -- "$replacement"
if [ -d "$target" ]; then
cp -aL -- "$target" "$replacement"
else
cp -L --preserve=mode,timestamps -- "$link" "$replacement"
fi
rm -rf -- "$link"
mv -- "$replacement" "$link"
done < <(find /tmp/iso -type l -print0)
find /tmp/iso -depth -type d -empty -delete
EOF
-
name: Generate release notes
if: env.exists == 'false'
id: notes
env:
LATEST_TAG: ${{ env.latest_tag }}
run: |
#!/usr/bin/env bash
set -Eeuo pipefail
python3 <<'PY'
import json
import os
import re
from collections import defaultdict
from pathlib import Path
iso_root = Path("/tmp/iso")
info_file = iso_root / "data/info.json"
changelog_file = Path("/tmp/rpm-changelog")
output_file = Path("/tmp/release-notes.md")
latest_tag = os.environ["LATEST_TAG"]
if not info_file.is_file():
raise SystemExit(
"ERROR: data/info.json was not found in the ISO."
)
if not changelog_file.is_file():
raise SystemExit(
"ERROR: RPM changelog was not generated."
)
changelog = changelog_file.read_text(
encoding="utf-8",
errors="replace",
)
changelog = changelog.replace("\r\n", "\n").replace("\r", "\n")
entries = [
entry.strip()
for entry in re.split(
r"(?=^\*[ \t])",
changelog,
flags=re.MULTILINE,
)
if entry.strip()
]
selected_entry = ""
for entry in entries:
if re.search(
rf"(?:^|[ \t-]){re.escape(latest_tag)}(?:\s|$)",
entry,
):
selected_entry = entry
break
if not selected_entry and entries:
selected_entry = entries[0]
selected_entry = re.sub(
r"^\*[ \t]*",
"",
selected_entry,
).strip()
header = ""
details = []
if selected_entry:
lines = selected_entry.splitlines()
header = lines[0].strip()
for line in lines[1:]:
line = line.strip()
if not line:
continue
details.append(
re.sub(r"^-[ \t]*", "", line)
)
lines = ["## RPM changelog", ""]
if header:
lines.append(header)
else:
lines.append(latest_tag)
if details:
lines.append("")
for detail in details:
lines.append(f"- {detail}")
windows_names = {
"2k25": "Windows Server 2025",
"w11": "Windows 11",
"2k22": "Windows Server 2022",
"2k19": "Windows Server 2019",
"2k16": "Windows Server 2016",
"w10": "Windows 10",
"2k12R2": "Windows Server 2012 R2",
"w8.1": "Windows 8.1",
"2k12": "Windows Server 2012",
"w8": "Windows 8",
"2k8R2": "Windows Server 2008 R2",
"w7": "Windows 7",
"2k8": "Windows Server 2008",
"2k3": "Windows Server 2003",
"xp": "Windows XP",
}
preferred_order = list(windows_names)
with info_file.open(encoding="utf-8") as file:
data = json.load(file)
grouped = defaultdict(lambda: defaultdict(set))
for driver in data.get("drivers", []):
if str(driver.get("arch", "")).lower() != "amd64":
continue
inf_path = str(
driver.get("inf_path", "")
).strip()
if not inf_path:
continue
inf = Path(inf_path)
if not inf.parts:
continue
component = inf.parts[0].lower()
# Ignore the separate legacy QEMU FWCfg package.
if component == "qemufwcfg":
continue
# Do not list drivers removed from the minified ISO.
if not (iso_root / inf).is_file():
continue
windows_version = str(
driver.get("windows_version", "")
).strip()
driver_version = str(
driver.get("driver_version", "")
).strip()
if not windows_version or not driver_version:
continue
# Remove the date preceding the actual driver version.
if "," in driver_version:
driver_version = driver_version.split(
",",
1,
)[1].strip()
component_name = str(
driver.get("name", component)
).strip()
if not component_name:
component_name = component
grouped[windows_version][driver_version].add(
component_name
)
windows_order = [
version
for version in preferred_order
if version in grouped
]
windows_order.extend(
sorted(
set(grouped) - set(windows_order),
key=str.casefold,
)
)
if grouped:
lines.extend(["", "## Driver versions"])
for windows_version in windows_order:
versions = grouped[windows_version]
heading = windows_names.get(
windows_version,
windows_version,
)
lines.extend(["", f"### {heading}", ""])
for driver_version in sorted(
versions,
reverse=True,
):
components = sorted(
versions[driver_version],
key=str.casefold,
)
lines.append(
f"- `{driver_version}` — "
f"{', '.join(components)}"
)
output_file.write_text(
"\n".join(lines).rstrip() + "\n",
encoding="utf-8",
)
PY
cat /tmp/release-notes.md
delimiter="RELEASE_NOTES_$(date +%s%N)"
{
echo "release_notes<<$delimiter"
cat /tmp/release-notes.md
echo "$delimiter"
} >> "$GITHUB_OUTPUT"
-
name: Deduplicate files
if: env.exists == 'false'
id: deduplicate
run: |
#!/usr/bin/env bash
set -Eeuo pipefail
# Reuse the Windows 8.1 viogpudo package for Windows 8.
sudo cp -al \
/tmp/iso/viogpudo/w8.1 \
/tmp/iso/viogpudo/w8
# Add the required WDF installer DLL using hard links.
wdf="/tmp/iso/vioserial/w7/amd64/WdfCoInstaller01009.dll"
sudo cp -l "$wdf" /tmp/iso/Balloon/xp/amd64/WdfCoInstaller01009.dll
sudo cp -l "$wdf" /tmp/iso/Balloon/2k3/amd64/WdfCoInstaller01009.dll
sudo cp -l "$wdf" /tmp/iso/Balloon/2k8/amd64/WdfCoInstaller01009.dll
sudo cp -l "$wdf" /tmp/iso/pvpanic/2k8/amd64/WdfCoInstaller01009.dll
sudo cp -l "$wdf" /tmp/iso/vioserial/xp/amd64/WdfCoInstaller01009.dll
sudo cp -l "$wdf" /tmp/iso/vioserial/2k3/amd64/WdfCoInstaller01009.dll
sudo cp -l "$wdf" /tmp/iso/vioserial/2k8/amd64/WdfCoInstaller01009.dll
# Replace all other byte-identical files with hard links.
sudo rdfind \
-outputname /dev/null \
-removeidentinode false \
-makehardlinks true \
/tmp/iso
-
name: Verify driver folders
if: env.exists == 'false'
id: verify
run: |
#!/usr/bin/env bash
set -Eeuo pipefail
missing="/tmp/missing-driver-folders.txt"
: > "$missing"
while IFS=: read -r driver versions; do
[ -n "$driver" ] || continue
for version in $versions; do
path="/tmp/iso/$driver/$version"
if [ ! -d "$path" ]; then
echo "$driver/$version" >> "$missing"
fi
done
done <<'EOF'
Balloon:2k3 xp 2k8 2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25
NetKVM:2k3 xp 2k8 2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25
fwcfg:2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25
pvpanic:2k8 2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25
qemufwcfg:2k16 2k19 w10 w11 2k22 2k25
qemupciserial:2k8 2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25
sata:xp
smbus:2k8
sriov:w8.1 2k12R2 2k16 2k19 w10 w11 2k22 2k25
viofs:w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25
viogpudo:w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25
vioinput:2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25
viomem:2k16 2k19 w10 w11 2k22 2k25
viorng:2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25
vioscsi:2k8 2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25
vioserial:2k3 xp 2k8 2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25
viosock:2k16 2k19 w10 w11 2k22 2k25
viostor:2k3 xp 2k8 2k8R2 w7 w8 w8.1 2k12 2k12R2 2k16 2k19 w10 w11 2k22 2k25
EOF
if [ -s "$missing" ]; then
echo "ERROR: Previously available driver folders are missing:" >&2
sed 's/^/ - /' "$missing" >&2
echo >&2
echo "The upstream package may have dropped legacy Windows drivers." >&2
echo "Add the missing drivers to legacy.zip before publishing this release." >&2
exit 1
fi
echo "All expected driver folders are present."
-
name: Build minified ISO
if: env.exists == 'false'
id: build
run: |
#!/usr/bin/env bash
set -Eeuo pipefail
base="${{ env.latest_base }}"
owner="$(id -u):$(id -g)"
sudo genisoimage \
-o "/tmp/virtio-win-${base}.iso" \
-r \
-iso-level 4 \
-input-charset iso8859-1 \
-cache-inodes \
-V "virtio-win-${base}" \
/tmp/iso
# Add a stable filename for the latest-release download URL.
cp \
"/tmp/virtio-win-${base}.iso" \
/tmp/virtio-win.iso
# Keep the MSI files in the ISO, but omit them from the archives.
sudo rm -f /tmp/iso/*.msi
sudo tar \
--owner=0 \
--group=0 \
--numeric-owner \
-C /tmp/iso \
-czf "/tmp/virtio-win-${base}.tar.gz" \
.
sudo env XZ_OPT=-9 tar \
--owner=0 \
--group=0 \
--numeric-owner \
-C /tmp/iso \
-cJf "/tmp/virtio-win-${base}.tar.xz" \
.
sudo chown "$owner" \
"/tmp/virtio-win-${base}.iso" \
"/tmp/virtio-win-${base}.tar.gz" \
"/tmp/virtio-win-${base}.tar.xz"
chown "$owner" /tmp/virtio-win.iso
-
name: Checkout
if: env.exists == 'false'
uses: actions/checkout@v7
with:
fetch-depth: 0
-
name: Create a release
if: env.exists == 'false'
uses: action-pack/github-release@v2
with:
body: ${{ steps.notes.outputs.release_notes }}
tag: "v${{ env.latest_tag }}"
title: "v${{ env.latest_base }}"
token: ${{ secrets.REPO_ACCESS_TOKEN }}
-
name: Update release
if: env.exists == 'false'
uses: AButler/upload-release-assets@v4.0
with:
files: '/tmp/virtio-win*'
release-tag: "v${{ env.latest_tag }}"
repo-token: ${{ secrets.REPO_ACCESS_TOKEN }}
-
name: Send mail
if: env.exists == 'false'
uses: action-pack/send-mail@v1
with:
to: ${{ secrets.MAILTO }}
from: Github Actions <${{ secrets.MAILTO }}>
connection_url: ${{ secrets.MAIL_CONNECTION }}
subject: Build of ${{ github.event.repository.name }} v${{ env.latest_tag }} completed
body: |
The build job of ${{ github.event.repository.name }} v${{ env.latest_tag }} was completed successfully!
See https://github.com/${{ github.repository }}/actions for more information.