Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
# back to their defaults.
PYTHONOS_BUILD_JOBS: ${{ matrix.build_jobs }}
PYTHONOS_GUI_COMMAND_TIMEOUT: ${{ matrix.gui_command_timeout }}
PYTHONOS_GUI_FRAME_TIMEOUT: ${{ matrix.gui_frame_timeout }}
strategy:
fail-fast: false
matrix:
Expand All @@ -51,6 +52,7 @@ jobs:
# Linux hosts.
build_jobs: "1"
gui_command_timeout: "240"
gui_frame_timeout: "240"
qemu_bin: qemu-system-x86_64
artifact: pythonos-macos-x86_64
artifact_path: build/pythonos.iso
Expand Down
40 changes: 38 additions & 2 deletions scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,34 @@ collect_ci_images() {
info "arm64 ELF: $RELEASE_ELF"
}

# Wrap a CI image in the versioned, checksummed bundle the sibling projects
# publish. RubyOS and RemoteOS-SDL ship <name>-<version>-<platform>.tar.gz plus
# a .sha256; PythonOS was attaching bare pythonos.iso / pythonos-arm64.elf with
# no version in the name and nothing to verify them against. The images here are
# guest media rather than host runtimes, so the platform field is the guest
# architecture and there are two of them, not three.
package_release_image() {
local image="$1" arch="$2" version="$3" dest="$4"
local bundle="pythonos-${version}-${arch}"
local stage
stage="$(mktemp -d "${TMPDIR:-/tmp}/pythonos-bundle.XXXXXX")"
mkdir -p "$stage/$bundle"
cp -f "$image" README.md LICENSE RELEASE-NOTES.md "$stage/$bundle/"
printf 'version=%s\ntarget=%s\nimage=%s\n' \
"$version" "$arch" "$(basename "$image")" > "$stage/$bundle/BUILD-INFO.txt"
tar -czf "$dest/$bundle.tar.gz" -C "$stage" "$bundle"
(
cd "$dest"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$bundle.tar.gz"
else
shasum -a 256 "$bundle.tar.gz"
fi
) > "$dest/$bundle.tar.gz.sha256"
rm -rf "$stage"
printf '%s/%s.tar.gz\n' "$dest" "$bundle"
}

main() {
local bump="${1:-patch}"
local previous
Expand Down Expand Up @@ -291,9 +319,17 @@ main() {

info "creating GitHub release $tag"
info "attaching CI images: $RELEASE_ISO $RELEASE_ELF"
local bundle_dir="$ASSET_DIR/bundles"
mkdir -p "$bundle_dir"
local x86_bundle arm_bundle
x86_bundle="$(package_release_image "$RELEASE_ISO" x86_64 "$version" "$bundle_dir")"
arm_bundle="$(package_release_image "$RELEASE_ELF" arm64 "$version" "$bundle_dir")"
info "packaged $x86_bundle"
info "packaged $arm_bundle"

gh release create "$tag" --title "$tag" --notes-file "$notes_file" \
"$RELEASE_ISO#pythonos.iso" \
"$RELEASE_ELF#pythonos-arm64.elf"
"$x86_bundle" "$x86_bundle.sha256" \
"$arm_bundle" "$arm_bundle.sha256"

info "release complete: $tag"
}
Expand Down
12 changes: 9 additions & 3 deletions tests/ci_gate_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,15 @@ def main() -> int:
"did not upload pythonos.iso" in release)
check("release.sh requires the arm64 ELF",
"did not upload pythonos-arm64.elf" in release)
check("release.sh attaches both images",
'"$RELEASE_ISO#pythonos.iso"' in release
and '"$RELEASE_ELF#pythonos-arm64.elf"' in release)
check("release.sh packages both images as versioned bundles",
'package_release_image "$RELEASE_ISO" x86_64' in release
and 'package_release_image "$RELEASE_ELF" arm64' in release
and 'bundle="pythonos-${version}-${arch}"' in release)
check("release.sh attaches a checksum beside every bundle",
'"$x86_bundle" "$x86_bundle.sha256"' in release
and '"$arm_bundle" "$arm_bundle.sha256"' in release)
check("release bundles carry the docs the other projects ship",
'cp -f "$image" README.md LICENSE RELEASE-NOTES.md' in release)
check("release requires current long-form release notes",
"validate_release_notes" in release
and "# PythonOS v$version" in release
Expand Down
11 changes: 8 additions & 3 deletions tests/desktop_smoke_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from qmp_helper import (
QemuMonitor, parse_ppm, sample_pixel, color_close,
tile_hashes, golden_check_or_refresh,
tile_hashes, golden_check_or_refresh, env_seconds,
)


Expand All @@ -31,7 +31,12 @@
MON = "/tmp/pythonos-desktop.mon.sock"
PPM = "/tmp/pythonos-desktop.ppm"
LOG = "/tmp/pythonos-desktop.log"
BOOT_TIMEOUT = float(os.environ.get("PYTHONOS_GUI_BOOT_TIMEOUT", "30"))
BOOT_TIMEOUT = env_seconds("PYTHONOS_GUI_BOOT_TIMEOUT", 30.0)
# How long to wait for the desktop's signature pixels to appear. Startup
# imports the app registry and decodes the background inside the guest, which
# under TCG on a VM-hosted runner can take several times what it costs on a
# Linux host.
FRAME_TIMEOUT = env_seconds("PYTHONOS_GUI_FRAME_TIMEOUT", 60.0)


def _qemu_cmd():
Expand Down Expand Up @@ -99,7 +104,7 @@ def main() -> int:
# background in the guest. Under TCG that takes much longer than
# under KVM, so wait for the window's signature pixels instead of
# assuming a fixed four-second startup time.
frame_deadline = time.time() + 60.0
frame_deadline = time.time() + FRAME_TIMEOUT
w = h = 0
rgb = b""
while time.time() < frame_deadline:
Expand Down
22 changes: 3 additions & 19 deletions tests/gui_smoke_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,29 +28,13 @@
# Allow `python3 tests/gui_smoke_test.py` from the repo root or anywhere.
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from qmp_helper import (
QemuMonitor, parse_ppm, sample_pixel, color_close,
QemuMonitor, parse_ppm, sample_pixel, color_close, env_seconds,
)

def _env_seconds(name: str, default: float) -> float:
"""Read a timeout from the environment, falling back on anything unusable.

A workflow that sets this from an unset matrix key passes an empty string
rather than leaving the variable out, and bare float("") raises. Treat
empty, non-numeric and non-positive values as "not configured" so a typo
cannot turn a timeout into a crash -- or into zero.
"""
raw = os.environ.get(name, "")
try:
value = float(raw)
except ValueError:
return default
return value if value > 0 else default


ISO = sys.argv[1] if len(sys.argv) > 1 else "build/pythonos.iso"
PORT = int(os.environ.get("PYTHONOS_GUI_HOST_PORT", "5559"))
BOOT_TIMEOUT = _env_seconds("PYTHONOS_GUI_BOOT_TIMEOUT", 30.0)
COMMAND_TIMEOUT = _env_seconds("PYTHONOS_GUI_COMMAND_TIMEOUT", 15.0)
BOOT_TIMEOUT = env_seconds("PYTHONOS_GUI_BOOT_TIMEOUT", 30.0)
COMMAND_TIMEOUT = env_seconds("PYTHONOS_GUI_COMMAND_TIMEOUT", 15.0)

SERIAL_LOG = "/tmp/pythonos-gui-smoke.log"
MONITOR_SOCK = "/tmp/pythonos-gui-smoke.mon.sock"
Expand Down
16 changes: 16 additions & 0 deletions tests/qmp_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@
import time


def env_seconds(name: str, default: float) -> float:
"""Read a timeout from the environment, falling back on anything unusable.

A workflow that sets one of these from a matrix key defined for only some
entries passes an empty string to the rest rather than leaving the variable
out, and bare float("") raises. Empty, non-numeric and non-positive values
all mean "not configured" here, so a typo cannot turn a timeout into a
crash -- or into zero.
"""
try:
value = float(os.environ.get(name, ""))
except ValueError:
return default
return value if value > 0 else default


class QemuMonitor:
"""Connection to a QEMU HMP monitor exposed at a Unix socket.

Expand Down
Loading